diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d01bdf6fe..6e7a050c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,74 +17,72 @@ on: jobs: + linters: + name: Linters + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + fail-fast: false + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up python + uses: actions/setup-python@v4 + with: + python-version: 3.8 + + - name: venv restore + id: cache-venv + uses: syphar/restore-virtualenv@v1 + with: + requirement_files: requirements.txt + + - name: venv create + if: steps.cache-venv.outputs.cache-hit != 'true' + run: pip install -e . -r requirements.txt + + - name: codespell + run: codespell + + - name: pylint + run: pylint --recursive=y examples pymodbus test + + - name: precommit (black, bandit, and ruff) + run: pre-commit run --all-files + + - name: docs + run: make -C doc/ html + + - name: mypy + run: mypy pymodbus + + integreation_test: - name: ${{ matrix.task.name }} - ${{ matrix.os.on }} - ${{ matrix.python.version }} + name: pytest - ${{ matrix.os.on }} - ${{ matrix.python.version }} runs-on: ${{ matrix.os.on }} + needs: linters timeout-minutes: 10 strategy: fail-fast: false matrix: - task: - - name: pylint - cmd: pylint --recursive=y examples pymodbus test - type: lint - - name: codespell - cmd: codespell - type: lint - - name: bandit - cmd: bandit -r -c bandit.yaml . - type: lint - - name: flake8 - cmd: flake8 - type: lint - - name: precommit (isort and black) - cmd: pre-commit run --all-files - type: lint - - name: docs - cmd: make -C doc/ html - type: lint - - name: mypy - cmd: mypy pymodbus - type: lint - - name: pytest - cmd: pytest --cov=pymodbus --cov=test --cov-report=term-missing --cov-report=xml -v --full-trace --timeout=20 - type: test os: - - name: Linux - on: ubuntu-latest - lint: 'yes' - - name: Macos - on: macos-latest - lint: 'no' - - name: Windows - on: windows-latest - lint: 'no' + - on: ubuntu-latest + - on: macos-latest + - on: windows-latest python: - version: '3.8' - lint: 'yes' - version: '3.9' - lint: 'no' - version: '3.10' - lint: 'no' - version: '3.11' - lint: 'no' - version: pypy-3.8 - lint: 'no' exclude: - - task: - type: lint - os: - lint: 'no' - - task: - type: lint - python: - lint: 'no' - os: - name: Macos + on: macos-latest python: version: pypy-3.8 - os: - name: Windows + on: windows-latest python: version: pypy-3.8 steps: @@ -106,5 +104,15 @@ jobs: if: steps.cache-venv.outputs.cache-hit != 'true' run: pip install -e . -r requirements.txt - - name: test/lint - run: ${{ matrix.task.cmd }} + - name: pytest + run: pytest --cov=pymodbus --cov=test --cov-report=term-missing --cov-report=xml -v --full-trace --timeout=20 + + ci_complete: + name: ci_complete + runs-on: ubuntu-latest + needs: + - integreation_test + timeout-minutes: 1 + steps: + - name: Dummy + run: ls diff --git a/.github/workflows/clean_cache.yml b/.github/workflows/clean_cache.yml deleted file mode 100644 index bce93d4c1..000000000 --- a/.github/workflows/clean_cache.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Clear cache - -on: - schedule: - # Monthly day 1 at 0:35 UTC. - - cron: '35 0 1 1-12 *' - workflow_dispatch: - -permissions: - actions: write - -jobs: - clear-cache: - runs-on: ubuntu-latest - steps: - - name: Clear cache - uses: actions/github-script@v6 - with: - script: | - console.log("About to clear") - const caches = await github.rest.actions.getActionsCacheList({ - owner: context.repo.owner, - repo: context.repo.repo, - }) - for (const cache of caches.data.actions_caches) { - console.log(cache) - github.rest.actions.deleteActionsCacheById({ - owner: context.repo.owner, - repo: context.repo.repo, - cache_id: cache.id, - }) - } - console.log("Clear completed") diff --git a/.github/workflows/clean_workflow_runs.yml b/.github/workflows/clean_workflow_runs.yml index ba41a4294..97a3effc4 100644 --- a/.github/workflows/clean_workflow_runs.yml +++ b/.github/workflows/clean_workflow_runs.yml @@ -1,18 +1,21 @@ name: Clean workflow runs on: + # schedule: + # # Monthly day 1 at 0:35 UTC. + # - cron: '35 0 1 1-12 *' workflow_dispatch: inputs: - days: + retain_days: description: 'retain days (default 14)' - required: false + required: true default: 14 - keeps: + keep_minimum_runs: description: 'keep minimum runs (default 6)' - required: false + required: true default: 6 - schedule: - # Sunday at 02:35 UTC. - - cron: '35 2 * * 0' + +permissions: + actions: write jobs: del_runs: @@ -23,5 +26,27 @@ jobs: with: token: ${{ github.token }} repository: ${{ github.repository }} - retain_days: ${{ github.events.inputs.days }} - keep_minimum_runs: ${{ github.events.inputs.keeps }} + retain_days: ${{ github.events.inputs.retain_days }} + keep_minimum_runs: ${{ github.events.inputs.keep_minimum_runs }} + + clear-cache: + runs-on: ubuntu-latest + steps: + - name: Clear cache + uses: actions/github-script@v6 + with: + script: | + console.log("About to clear") + const caches = await github.rest.actions.getActionsCacheList({ + owner: context.repo.owner, + repo: context.repo.repo, + }) + for (const cache of caches.data.actions_caches) { + console.log(cache) + github.rest.actions.deleteActionsCacheById({ + owner: context.repo.owner, + repo: context.repo.repo, + cache_id: cache.id, + }) + } + console.log("Clear completed") diff --git a/.github/workflows/lock.yaml b/.github/workflows/lock.yaml new file mode 100644 index 000000000..ba046f481 --- /dev/null +++ b/.github/workflows/lock.yaml @@ -0,0 +1,24 @@ +name: 'Lock Threads' + +on: + schedule: + # Every night at 02:20 UTC. + - cron: '20 2 * * *' + workflow_dispatch: + +permissions: + issues: write + pull-requests: write + +concurrency: + group: lock + +jobs: + action: + runs-on: ubuntu-latest + steps: + - uses: dessant/lock-threads@v4 + with: + github-token: ${{ github.token }} + issue-inactive-days: '10' + pr-inactive-days: '10' diff --git a/.github/workflows/publish1.yml.NO_RUN b/.github/workflows/publish1.yml.NO_RUN deleted file mode 100644 index 923c7eab6..000000000 --- a/.github/workflows/publish1.yml.NO_RUN +++ /dev/null @@ -1,27 +0,0 @@ -name: TEST Upload Python Package - -on: - release: - # types: [created] - branches: ["no"] - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: Set up Python - uses: actions/setup-python@v1 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine - - name: Build and publish - env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - python setup.py sdist bdist_wheel - twine upload dist/* diff --git a/.github/workflows/publish2.yml.NO_RUN b/.github/workflows/publish2.yml.NO_RUN deleted file mode 100644 index ea8ac4fd9..000000000 --- a/.github/workflows/publish2.yml.NO_RUN +++ /dev/null @@ -1,27 +0,0 @@ -name: TEST2 Upload Python Package - -on: - release: -# types: [created] - branches: ["no"] - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: Set up Python - uses: actions/setup-python@v1 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python3 -m pip install --upgrade pip - pip install setuptools wheel twine - - name: Build and publish - env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - python3 setup.py sdist bdist_wheel - twine upload dist/* diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index f8d233527..2b77d44a0 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -3,12 +3,11 @@ name: Mark stale issues and pull requests on: schedule: - cron: "30 1 * * *" + workflow_dispatch: jobs: stale: - runs-on: ubuntu-latest - steps: - uses: actions/stale@v3 with: @@ -22,9 +21,6 @@ jobs: days-before-issue-close: 5 days-before-pr-close: 10 stale-issue-label: 'no-issue-activity' - exempt-issue-labels: 'Bug,Enhancements,Investigating,in progress,Documentation Update Required,3.x' + exempt-issue-labels: 'Bug,Enhancements,Investigating' stale-pr-label: 'no-pr-activity' - exempt-pr-labels: 'IN REVIEW,Reviewing,Draft,in progress,3.x,2.5.0' remove-stale-when-updated: true -# only-labels: "More Information Required, Not an Issue, question, Won't Do" - diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 315f0d6f1..2ae198d03 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,21 +1,24 @@ -# See https://pre-commit.com for more information -# See https://pre-commit.com/hooks.html for more hooks +--- +default_language_version: + python: python + repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.2.0 + rev: v4.4.0 hooks: - id: trailing-whitespace - # - id: end-of-file-fixer + - id: end-of-file-fixer - id: check-yaml - id: check-added-large-files -- repo: https://github.com/pycqa/isort - rev: 5.12.0 +# run ruff with --fix before black +- repo: https://github.com/charliermarsh/ruff-pre-commit + rev: 'v0.0.263' hooks: - - id: isort - exclude: ^(doc/_build|venv|.venv|.git|pymodbus/client/serial_asyncio) + - id: ruff + args: [--fix, --exit-non-zero-on-fix] - repo: https://github.com/psf/black - rev: 22.12.0 + rev: 23.3.0 hooks: - id: black - args: [--safe,--quiet] + args: [--safe, --quiet] files: (examples|pymodbus|test)/ diff --git a/.pydevproject b/.pydevproject deleted file mode 100644 index c50f6cda2..000000000 --- a/.pydevproject +++ /dev/null @@ -1,8 +0,0 @@ - - - -/pymodbus - -python 3.8 -Pymodbus Environment - diff --git a/API_changes.rst b/API_changes.rst index 43ad2ccda..92f70ac8a 100644 --- a/API_changes.rst +++ b/API_changes.rst @@ -2,6 +2,17 @@ PyModbus - API changes. ======================= +------------- +Version 3.3.0 +------------- +- ModbusTcpDiagClient is removed due to lack of support +- Clients have an optional parameter: on_reconnect_callback, Function that will be called just before a reconnection attempt. +- general parameter unit= -> slave= +- move SqlSlaveContext, RedisSlaveContext to examples/contrib (due to lack of maintenance) +- :code:`BinaryPayloadBuilder.to_string` was renamed to :code:`BinaryPayloadBuilder.encode` +- on_reconnect_callback for async clients works slightly different +- utilities/unpack_bitstring now expects an argument named `data` not `string` + ------------- Version 3.2.0 ------------- diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 341ad2302..7626dfa0a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,64 @@ +version 3.3.0 +---------------------------------------------------------- +* Stabilize windows tests. (#1567) +* Bump mypy 1.3.0 (#1568) +* Transport integrated in async clients. (#1541) +* Client async corrections (due to 3.1.2) (#1565) +* Server_async[udp], solve 3.1.1 problem. (#1564) +* Remove ModbusTcpDiagClient. (#1560) +* Remove old method from Python2/3 transition (#1559) +* Switch to ruff's version of bandit (#1557) +* Allow reading/writing address 0 in the simulator (#1552) +* Remove references to "defer_start". (#1548) +* Client more robust against faulty response. (#1547) +* Fix missing package_data directives for simulator web (#1544) +* Fix installation instructions (#1543) +* Solve pytest timeout problem. (#1540) +* DiagnosticStatus encode missing tuple check. (#1533) +* test SparseDataStore. (#1532) +* BinaryPayloadBuilder.to_string to BinaryPayloadBuilder.encode (#1526) +* Adding flake8-pytest-style` to ruff (#1520) +* Simplify version management. (#1522) +* pylint and pre-commit autoupdate (#1519) +* Add type hint (#1512) +* Add action to lock issues/PR. (#1508) +* New common transport layer. (#1492) +* Solve serial close raise problem. +* Remove old config values (#1503) +* Document pymodbus.simulator. (#1502) +* Refactor REPL server to reduce complexity (#1499) +* Don't catch KeyboardInterrupt twice for REPL server (#1498) +* Refactor REPL client to reduce complexity (#1489) +* pymodbus.server: listen on ID 1 by default (#1496) +* Clean framer/__init__.py (#1494) +* Duplicate transactions in UDP. (#1486) +* clean ProcessIncommingPacket. (#1491) +* Enable pyupgrade (U) rules in ruff (#1484) +* clean_workflow.yaml solve parameter problem. +* Correct wrong import in test. (#1483) +* Implement pyflakes-simplify (#1480) +* Test case for UDP duplicate msg issue (#1470) +* Test of write_coil. (#1479) +* Test reuse of client object. (#1475) +* Comment about addressing when shared=false (#1474) +* Remove old aliases to OSError (#1473) +* pymodbus.simulator fixes (#1463) +* Fix wrong error message with pymodbus console (#1456) +* update modbusrtuframer (#1435) +* Server multidrop test.: (#1451) +* mypy problem ModbusResponse. + +Thanks to: + Alex + Christian Krause + corollaries + dhoomakethu + Ghostkeeper + jan iversen + James Braza + Kenny Johansson + Pavel Kostromitinov + version 3.2.2 (picked from dev, only bugfixes) ---------------------------------------------------------- * Add forgotten await diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..fa1d2ad03 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,3 @@ +Just fork the repo and raise your PR against `dev` branch. + +We always have more work than time, so feel free to open a discussion / issue on a theme you want to solve. diff --git a/LICENSE b/LICENSE index 1a938446d..862459c66 100644 --- a/LICENSE +++ b/LICENSE @@ -22,4 +22,3 @@ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - diff --git a/MAKE_RELEASE.rst b/MAKE_RELEASE.rst index b2663e3e5..761060dd4 100644 --- a/MAKE_RELEASE.rst +++ b/MAKE_RELEASE.rst @@ -8,13 +8,13 @@ Making a release. ------------------------------------------------------------ Prepare/make release on dev. ------------------------------------------------------------ -* Make pull request "prepare v3.0.x", with the following: - * Update pymodbus/version.py with version number (last line) +* Make pull request "prepare v3.3.x", with the following: + * Update pymodbus/__init__.py with version number (__version__ X.Y.Zpre) * Update README.rst "Supported versions" * Update CHANGELOG.rst * Add commits from last release, but selectively ! - git log --oneline v3.2.0..HEAD > commit.log - git log v3.2.0..HEAD | grep Author > contributors.log + git log --oneline v3.2.2..HEAD > commit.log + git log v3.2.2..HEAD | grep Author > contributors.log * Commit, push and merge. * Checkout master locally * git merge dev @@ -38,5 +38,6 @@ Prepare/make release on dev. ------------------------------------------------------------ Prepare release on dev for new commits. ------------------------------------------------------------ +* git branch -D master * Make pull request "prepare dev", with the following: * Update pymodbus/version.py with version number (last line) diff --git a/MANIFEST.in b/MANIFEST.in index 9e8f9ae0b..cc2c00f0c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ include requirements.txt include README.rst include CHANGELOG.rst -include LICENSE \ No newline at end of file +include LICENSE diff --git a/README.rst b/README.rst index ce63c03ca..680d32c99 100644 --- a/README.rst +++ b/README.rst @@ -20,9 +20,9 @@ The move from a company organization to pymodbus-dev was done to allow a 100% op Supported versions ------------------------------------------------------------ -Version `2.5.3 `_ is the last 2.x release (Supports python 2.7.x - 3.7). +Version `2.5.3 `_ is the last 2.x release (Supports python >= 2.7, no longer supported). -Version `3.2.2 `_ is the current release (Supports Python >=3.8). +Version `3.3.0 `_ is the current release (Supports Python >= 3.8). .. important:: All API changes after 3.0.0 are documented in `API_changes.rst `_ @@ -70,7 +70,7 @@ Server Features * TCP, RTU-OVER-TCP, UDP, TLS, Serial ASCII, Serial RTU, and Serial Binary * asynchronous and synchronous versions * Full server control context (device information, counters, etc) - * A number of backend contexts (database, redis, sqlite, a slave device) as datastore + * A number of backend contexts as datastore ^^^^^^^^^^^ Use Cases @@ -179,8 +179,6 @@ Available options are: - **serial**, installs serial drivers. -- **datastore**, installs databases (SQLAlchemy and Redis) for datastore. - - **documentation**, installs tools to generate documentation. - **development**, installs development tools needed to enable test/check of pymodbus changes. @@ -247,7 +245,7 @@ Install with Docker ----------------------------------------------------------- Pull the latest image on ``dev`` branch with ``docker pull ghcr.io/pymodbus-dev/pymodbus:dev``:: - doker pull ghcr.io/pymodbus-dev/pymodbus:dev + ❯ docker pull ghcr.io/pymodbus-dev/pymodbus:dev dev: Pulling from pymodbus-dev/pymodbus 548fcab5fe88: Pull complete a4d3f9f008ef: Pull complete @@ -369,8 +367,7 @@ Contributing ------------------------------------------------------------ Just fork the repo and raise your PR against `dev` branch. -Here are some of the items waiting to be done: - https://github.com/pymodbus-dev/pymodbus/blob/dev/doc/TODO +We always have more work than time, so feel free to open a discussion / issue on a theme you want to solve. ------------------------------------------------------------ License Information diff --git a/TODO b/TODO deleted file mode 120000 index 9cd0898d6..000000000 --- a/TODO +++ /dev/null @@ -1 +0,0 @@ -./doc/TODO \ No newline at end of file diff --git a/TODO_add_checks b/TODO_add_checks deleted file mode 100644 index 6968dfb24..000000000 --- a/TODO_add_checks +++ /dev/null @@ -1,2 +0,0 @@ -New checks -- prettier diff --git a/TODO_pylint b/TODO_pylint deleted file mode 100644 index 07630fec7..000000000 --- a/TODO_pylint +++ /dev/null @@ -1,64 +0,0 @@ -pylint general: -see .pylintrc [MESSAGES CONTROL] - -pylint disables: - -R0801 -abstract-method -anomalous-backslash-in-string -arguments-differ -arguments-renamed -assignment-from-none -attribute-defined-outside-init -bare-except -broad-except -compare-to-zero -confusing-consecutive-elif -consider-iterating-dictionary -consider-using-dict-comprehension -consider-using-dict-items -consider-using-f-string -consider-using-namedtuple-or-dataclass -consider-using-with -dangerous-default-value -deprecated-module -disable-all -eq-without-hash -expression-not-assigned -fixme -global-variable-not-assigned -import-error -import-outside-toplevel -invalid-name -invalid-overridden-method -line-too-long -no-member -no-value-for-parameter -non-iterator-returned -protected-access -raise-missing-from -redefined-outer-name -signature-differs -simplifiable-if-expression -simplifiable-if-statement -super-init-not-called -too-complex -too-few-public-methods -too-many-arguments -too-many-branches -too-many-function-args -too-many-instance-attributes -too-many-lines -too-many-locals -too-many-nested-blocks -too-many-public-methods -too-many-statements -try-except-raise -unexpected-keyword-arg -unnecessary-comprehension -unnecessary-lambda -unspecified-encoding -unused-private-member -unused-variable -use-implicit-booleaness-not-len -use-maxsplit-arg diff --git a/bandit.yaml b/bandit.yaml deleted file mode 100644 index b3b28c019..000000000 --- a/bandit.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# https://bandit.readthedocs.io/en/latest/config.html -exclude_dirs: - - doc - - .venv - - venv - - .git -tests: - - B103 - - B108 - - B306 - - B307 - - B313 - - B314 - - B315 - - B316 - - B317 - - B318 - - B319 - - B320 - - B325 - - B601 - - B602 - - B604 - - B608 - - B609 diff --git a/check_ci.sh b/check_ci.sh index ed486d38b..4a6365c8b 100755 --- a/check_ci.sh +++ b/check_ci.sh @@ -8,7 +8,6 @@ trap 'echo "\"${last_command}\" command filed with exit code $?."' EXIT codespell pre-commit run --all-files pylint --recursive=y examples pymodbus test -flake8 mypy pymodbus pytest --numprocesses auto echo "Ready to push" diff --git a/doc/changelog.rst b/doc/changelog.rst index 5f879e1e5..1c8b69f1e 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -2,4 +2,4 @@ CHANGELOGS ============ -.. include:: ../CHANGELOG.rst \ No newline at end of file +.. include:: ../CHANGELOG.rst diff --git a/doc/readme.rst b/doc/readme.rst index 6b2b3ec68..72a335581 100644 --- a/doc/readme.rst +++ b/doc/readme.rst @@ -1 +1 @@ -.. include:: ../README.rst \ No newline at end of file +.. include:: ../README.rst diff --git a/doc/source/examples.rst b/doc/source/examples.rst index e89656f01..a29d2f9e2 100644 --- a/doc/source/examples.rst +++ b/doc/source/examples.rst @@ -10,147 +10,77 @@ These examples are considered essential usage examples, and are guaranteed to wo because they are tested automatilly with each dev branch commit using CI. -Asynchronous Client Example -^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Asynchronous client +^^^^^^^^^^^^^^^^^^^ .. literalinclude:: ../../examples/client_async.py -Asynchronous Client basic calls example -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Asynchronous client basic calls +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. literalinclude:: ../../examples/client_calls.py -Modbus Payload Example -^^^^^^^^^^^^^^^^^^^^^^ -.. literalinclude:: ../../examples/client_payload.py +Asynchronous server +^^^^^^^^^^^^^^^^^^^ +.. literalinclude:: ../../examples/server_async.py -Synchronous Client Example -^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. literalinclude:: ../../examples/client_sync.py +Build bcd Payload +^^^^^^^^^^^^^^^^^ +.. literalinclude:: ../../examples/build_bcd_payload.py + +Callback Server example +^^^^^^^^^^^^^^^^^^^^^^^ +.. literalinclude:: ../../examples/server_callback.py + +Custom Message client +^^^^^^^^^^^^^^^^^^^^^ +.. literalinclude:: ../../examples/client_custom_msg.py -Forwarder Example +Message generator ^^^^^^^^^^^^^^^^^ +.. literalinclude:: ../../examples/message_generator.py + +Message Parser +^^^^^^^^^^^^^^ +.. literalinclude:: ../../examples/message_parser.py + +Modbus forwarder +^^^^^^^^^^^^^^^^ .. literalinclude:: ../../examples/modbus_forwarder.py -Asynchronous server example -^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. literalinclude:: ../../examples/server_async.py +Modbus payload client +^^^^^^^^^^^^^^^^^^^^^ +.. literalinclude:: ../../examples/client_payload.py -Modbus Payload Server example -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Modbus payload Server +^^^^^^^^^^^^^^^^^^^^^ .. literalinclude:: ../../examples/server_payload.py -Synchronous server example -^^^^^^^^^^^^^^^^^^^^^^^^^^ +Synchronous client +^^^^^^^^^^^^^^^^^^ +.. literalinclude:: ../../examples/client_sync.py + +Synchronous server +^^^^^^^^^^^^^^^^^^ .. literalinclude:: ../../examples/server_sync.py -Updating server example -^^^^^^^^^^^^^^^^^^^^^^^^^^ +Updating server +^^^^^^^^^^^^^^^ .. literalinclude:: ../../examples/server_updating.py -Modbus Simulator example -^^^^^^^^^^^^^^^^^^^^^^^^ -.. literalinclude:: ../../examples/v2.5.3/modbus_simulator.py - Examples contributions ---------------------- These examples are supplied by users of pymodbus. -The pymodbus team thanks for making the examples available to the community. - -Serial Forwarder example -^^^^^^^^^^^^^^^^^^^^^^^^ -.. literalinclude:: ../../examples/contrib/serial_forwarder.py - - - -Examples version 2.5.3 ----------------------- +The pymodbus team thanks for sharing the examples. -These examples have not been upgraded to v3.0.0 but are still relevant. +Redis datastore +^^^^^^^^^^^^^^^ +.. literalinclude:: ../../examples/contrib/redis_datastore.py -Help is wanted to upgrade the examples. - - -Asynchronous Asyncio Modbus TLS Client example -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. literalinclude:: ../../examples/v2.5.3/asynchronous_asyncio_modbus_tls_client.py - -Bcd Payload example -^^^^^^^^^^^^^^^^^^^ -.. literalinclude:: ../../examples/v2.5.3/bcd_payload.py - -Callback Server example -^^^^^^^^^^^^^^^^^^^^^^^ -.. literalinclude:: ../../examples/v2.5.3/callback_server.py - -Changing Framers example -^^^^^^^^^^^^^^^^^^^^^^^^ -.. literalinclude:: ../../examples/v2.5.3/changing_framers.py - -Concurrent Client example -^^^^^^^^^^^^^^^^^^^^^^^^^ -.. literalinclude:: ../../examples/v2.5.3/concurrent_client.py - -Custom Datablock example -^^^^^^^^^^^^^^^^^^^^^^^^ -.. literalinclude:: ../../examples/v2.5.3/custom_datablock.py - -Custom Message example -^^^^^^^^^^^^^^^^^^^^^^ -.. literalinclude:: ../../examples/v2.5.3/custom_message.py - -Dbstore update Server example -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. literalinclude:: ../../examples/v2.5.3/dbstore_update_server.py - -Deviceinfo showcase client example -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. literalinclude:: ../../examples/v2.5.3/deviceinfo_showcase_client.py - -Deviceinfo showcase server example -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. literalinclude:: ../../examples/v2.5.3/deviceinfo_showcase_server.py - -Libmodbus Client example -^^^^^^^^^^^^^^^^^^^^^^^^ -.. literalinclude:: ../../examples/v2.5.3/libmodbus_client.py - -Message Generator example -^^^^^^^^^^^^^^^^^^^^^^^^^ -.. literalinclude:: ../../examples/v2.5.3/message_generator.py - -Message Parser example -^^^^^^^^^^^^^^^^^^^^^^ -.. literalinclude:: ../../examples/v2.5.3/message_parser.py - -Modbus Logging example -^^^^^^^^^^^^^^^^^^^^^^ -.. literalinclude:: ../../examples/v2.5.3/modbus_logging.py - -Modbus Mapper example -^^^^^^^^^^^^^^^^^^^^^ -.. literalinclude:: ../../examples/v2.5.3/modbus_mapper.py +Serial Forwarder +^^^^^^^^^^^^^^^^ +.. literalinclude:: ../../examples/contrib/serial_forwarder.py -Modbus Saver example +Sqlalchemy datastore ^^^^^^^^^^^^^^^^^^^^ -.. literalinclude:: ../../examples/v2.5.3/modbus_saver.py - -Modbus Tls client example -^^^^^^^^^^^^^^^^^^^^^^^^^ -.. literalinclude:: ../../examples/v2.5.3/modbus_tls_client.py - -Modicon Payload example -^^^^^^^^^^^^^^^^^^^^^^^ -.. literalinclude:: ../../examples/v2.5.3/modicon_payload.py - -performance module -^^^^^^^^^^^^^^^^^^ -.. literalinclude:: ../../examples/v2.5.3/performance.py - -Remote Server Context example -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. literalinclude:: ../../examples/v2.5.3/remote_server_context.py - -Thread Safe Datastore example -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. literalinclude:: ../../examples/v2.5.3/thread_safe_datastore.py +.. literalinclude:: ../../examples/contrib/sql_datastore.py diff --git a/doc/source/library/REPL.rst b/doc/source/library/REPL.rst index 8f6a38d79..819aa8363 100644 --- a/doc/source/library/REPL.rst +++ b/doc/source/library/REPL.rst @@ -213,7 +213,7 @@ value are to be supplied in ``arg=val`` format. :: - > client.read_holding_registers count=4 address=9 unit=1 + > client.read_holding_registers count=4 address=9 slave=1 { "registers": [ 60497, diff --git a/doc/source/library/client.rst b/doc/source/library/client.rst index 156aec77f..b4ff6ea27 100644 --- a/doc/source/library/client.rst +++ b/doc/source/library/client.rst @@ -7,12 +7,8 @@ Pymodbus offers clients with transport protocols for - *TCP* - *TLS* - *UDP* -- possibility to add a custom transport protocol -communication in 2 versions: - -- :mod:`synchronous client`, -- :mod:`asynchronous client` using asyncio. +communication can be either using a :mod:`synchronous client` or a :mod:`asynchronous client` using asyncio. Using pymodbus client to set/get information from a device (server) is done in a few simple steps, like the following synchronous example:: diff --git a/doc/source/library/datastore.rst b/doc/source/library/datastore.rst index e09e8a449..3c1b570d7 100644 --- a/doc/source/library/datastore.rst +++ b/doc/source/library/datastore.rst @@ -22,13 +22,3 @@ Datastore classes .. autoclass:: pymodbus.datastore.ModbusSimulatorContext :members: :member-order: bysource - -.. autoclass:: pymodbus.datastore.RedisSlaveContext - :members: - :undoc-members: - :show-inheritance: - -.. autoclass:: pymodbus.datastore.SqlSlaveContext - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/source/library/framer.rst b/doc/source/library/framer.rst index 6daf4e2ee..c5d702db6 100644 --- a/doc/source/library/framer.rst +++ b/doc/source/library/framer.rst @@ -32,12 +32,3 @@ pymodbus\.framer\.socket_framer module :members: :undoc-members: :show-inheritance: - - -Module contents ---------------- - -.. automodule:: pymodbus.framer - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/source/library/simulator/config.rst b/doc/source/library/simulator/config.rst index d663a621f..b31ea48d6 100644 --- a/doc/source/library/simulator/config.rst +++ b/doc/source/library/simulator/config.rst @@ -252,6 +252,20 @@ Example "setup" configuration: Defines if the blocks are independent or shared (true) + .. tip:: + if shared is set to false, please remember to adjust the addresses, depending on in which group they are. + + assuming all sizes are set to 10, the addresses for configuration are as follows: + - coils have addresses 0-9, + - discrete_inputs have addresses 10-19, + - holding_registers have addresses 20-29, + - input_registers have addresses 30-39 + + when configuring the the datatypes (when calling each block start with 0). + + This is needed because the datatypes can be in different blocks. + + **"type exception"** Defines is the server returns a modbus exception if a read/write request violates the specified type. @@ -295,8 +309,6 @@ In the example registers 5, 10, 11, 12, 13, 14, 15 will produce an exception res Registers can be singulars (first entry) or arrays (second entry) - - Write section ^^^^^^^^^^^^^ diff --git a/doc/source/library/simulator/web.rst b/doc/source/library/simulator/web.rst index bf91a2ce5..b4c2cc252 100644 --- a/doc/source/library/simulator/web.rst +++ b/doc/source/library/simulator/web.rst @@ -8,6 +8,8 @@ TO BE DOCUMENTED. pymodbus.simulator ------------------ +The easiest way to run the simulator with web is to use "pymodbus.simulator" from the commandline. + TO BE DOCUMENTED. diff --git a/examples/v2.5.3/bcd_payload.py b/examples/build_bcd_payload.py similarity index 86% rename from examples/v2.5.3/bcd_payload.py rename to examples/build_bcd_payload.py index 6d877fccc..0479a6e4a 100644 --- a/examples/v2.5.3/bcd_payload.py +++ b/examples/build_bcd_payload.py @@ -1,10 +1,10 @@ -# pylint: disable=missing-type-doc,missing-param-doc,differing-param-doc,missing-raises-doc,missing-any-param-doc """Modbus BCD Payload Builder. This is an example of building a custom payload builder that can be used in the pymodbus library. Below is a simple binary coded decimal builder and decoder. """ + from struct import pack from pymodbus.constants import Endian @@ -13,10 +13,10 @@ from pymodbus.utilities import pack_bitstring, unpack_bitstring -def convert_to_bcd(decimal): +def convert_to_bcd(decimal: float) -> int: """Convert a decimal value to a bcd value - :param value: The decimal value to to pack into bcd + :param decimal: The decimal value to to pack into bcd :returns: The number in bcd form """ place, bcd = 0, 0 @@ -28,10 +28,10 @@ def convert_to_bcd(decimal): return bcd -def convert_from_bcd(bcd): +def convert_from_bcd(bcd: int) -> int: """Convert a bcd value to a decimal value - :param value: The value to unpack from bcd + :param bcd: The value to unpack from bcd :returns: The number in decimal form """ place, decimal = 1, 0 @@ -43,7 +43,7 @@ def convert_from_bcd(bcd): return decimal -def count_bcd_digits(bcd): +def count_bcd_digits(bcd: int) -> int: """Count the number of digits in a bcd value :param bcd: The bcd number to count the digits of @@ -98,25 +98,26 @@ def build(self): """ string = str(self) length = len(string) - string = string + ("\x00" * (length % 2)) + string += "\x00" * (length % 2) return [string[i : i + 2] for i in range(0, length, 2)] - def add_bits(self, values): + def add_bits(self, values: int) -> None: """Add a collection of bits to be encoded If these are less than a multiple of eight, they will be left padded with 0 bits to make it so. - :param value: The value to add to the buffer + :param values: The value to add to the buffer """ value = pack_bitstring(values) self._payload.append(value) - def add_number(self, value, size=None): + def add_number(self, value: int, size: int = None): """Add any 8bit numeric type to the buffer :param value: The value to add to the buffer + :param size: Size of buffer """ encoded = [] value = convert_to_bcd(value) @@ -128,7 +129,7 @@ def add_number(self, value, size=None): size -= 1 self._payload.extend(encoded) - def add_string(self, value): + def add_string(self, value: str): """Add a string to the buffer :param value: The value to add to the buffer @@ -155,7 +156,7 @@ def __init__(self, payload): self._pointer = 0x00 @staticmethod - def fromRegisters(registers, endian=Endian.Little): # pylint: disable=invalid-name + def fromRegisters(registers: int, endian: str = Endian.Little): """Initialize a payload decoder with the result of reading a collection of registers from a modbus device. @@ -167,6 +168,7 @@ def fromRegisters(registers, endian=Endian.Little): # pylint: disable=invalid-n :param registers: The register results to initialize with :param endian: The endianness of the payload :returns: An initialized PayloadDecoder + :raises ParameterException: parameter exception """ if isinstance(registers, list): # repack into flat binary payload = "".join(pack(">H", x) for x in registers) @@ -174,7 +176,7 @@ def fromRegisters(registers, endian=Endian.Little): # pylint: disable=invalid-n raise ParameterException("Invalid collection of registers supplied") @staticmethod - def fromCoils(coils, endian=Endian.Little): # pylint: disable=invalid-name + def fromCoils(coils: int, endian: str = Endian.Little): """Initialize a payload decoder. with the result of reading a collection of coils from a modbus device. @@ -184,6 +186,7 @@ def fromCoils(coils, endian=Endian.Little): # pylint: disable=invalid-name :param coils: The coil results to initialize with :param endian: The endianness of the payload :returns: An initialized PayloadDecoder + :raises ParameterException: parameter exception """ if isinstance(coils, list): payload = pack_bitstring(coils) @@ -206,7 +209,7 @@ def decode_bits(self): handle = self._payload[self._pointer - 1 : self._pointer] return unpack_bitstring(handle) - def decode_string(self, size=1): + def decode_string(self, size: int = 1): """Decode a string from the buffer :param size: The size of the string to decode @@ -215,8 +218,5 @@ def decode_string(self, size=1): return self._payload[self._pointer - size : self._pointer] -# --------------------------------------------------------------------------- # -# Exported Identifiers -# --------------------------------------------------------------------------- # - -__all__ = ["BcdPayloadBuilder", "BcdPayloadDecoder"] +if __name__ == "__main__": + print("Test") diff --git a/examples/client_async.py b/examples/client_async.py index 7ee456965..91206fa3b 100755 --- a/examples/client_async.py +++ b/examples/client_async.py @@ -40,8 +40,9 @@ _logger = logging.getLogger() -def setup_async_client(args): +def setup_async_client(description=None, cmdline=None): """Run client setup.""" + args = get_commandline(server=False, description=description, cmdline=cmdline) _logger.info("### Create client object") if args.comm == "tcp": client = AsyncModbusTcpClient( @@ -120,17 +121,33 @@ async def run_async_client(client, modbus_calls=None): """Run sync client.""" _logger.info("### Client starting") await client.connect() + print("jan " + str(client.connected)) assert client.connected if modbus_calls: await modbus_calls(client) - await client.close() + client.close() _logger.info("### End of Program") -if __name__ == "__main__": - cmd_args = get_commandline( - server=False, - description="Run asynchronous client.", +async def helper(): + """Combine the setup and run""" + args = [ + "--comm", + "udp", + "--host", + "127.0.0.1", + "--port", + "5020", + "--framer", + "socket", + "--log", + "debug", + ] + testclient = setup_async_client( + description="Run asynchronous client.", cmdline=args ) - testclient = setup_async_client(cmd_args) - asyncio.run(run_async_client(testclient), debug=True) + await run_async_client(testclient) + + +if __name__ == "__main__": + asyncio.run(helper(), debug=True) diff --git a/examples/client_calls.py b/examples/client_calls.py index 7a1135e07..0a7f6134e 100755 --- a/examples/client_calls.py +++ b/examples/client_calls.py @@ -42,7 +42,6 @@ import pymodbus.other_message as req_other from examples.client_async import run_async_client, setup_async_client from examples.client_sync import run_sync_client, setup_sync_client -from examples.helper import get_commandline from pymodbus.exceptions import ModbusException from pymodbus.pdu import ExceptionResponse @@ -178,11 +177,11 @@ async def _handle_holding_registers(client): "read_address": 1, "read_count": 8, "write_address": 1, - "write_registers": [256, 128, 100, 50, 25, 10, 5, 1], + "values": [256, 128, 100, 50, 25, 10, 5, 1], } _check_call(await client.readwrite_registers(slave=SLAVE, **arguments)) rr = _check_call(await client.read_holding_registers(1, 8, slave=SLAVE)) - assert rr.registers == arguments["write_registers"] + assert rr.registers == arguments["values"] async def _handle_input_registers(client): @@ -196,71 +195,77 @@ async def _execute_information_requests(client): """Execute extended information requests.""" _logger.info("### Running information requests.") rr = _check_call( - await client.execute(req_mei.ReadDeviceInformationRequest(unit=SLAVE)) + await client.execute(req_mei.ReadDeviceInformationRequest(slave=SLAVE)) ) assert rr.information[0] == b"Pymodbus" - rr = _check_call(await client.execute(req_other.ReportSlaveIdRequest(unit=SLAVE))) + rr = _check_call(await client.execute(req_other.ReportSlaveIdRequest(slave=SLAVE))) assert rr.status rr = _check_call( - await client.execute(req_other.ReadExceptionStatusRequest(unit=SLAVE)) + await client.execute(req_other.ReadExceptionStatusRequest(slave=SLAVE)) ) assert not rr.status rr = _check_call( - await client.execute(req_other.GetCommEventCounterRequest(unit=SLAVE)) + await client.execute(req_other.GetCommEventCounterRequest(slave=SLAVE)) ) - assert rr.status and not rr.count + assert rr.status + assert not rr.count - rr = _check_call(await client.execute(req_other.GetCommEventLogRequest(unit=SLAVE))) - assert rr.status and not (rr.event_count + rr.message_count + len(rr.events)) + rr = _check_call( + await client.execute(req_other.GetCommEventLogRequest(slave=SLAVE)) + ) + assert rr.status + assert not (rr.event_count + rr.message_count + len(rr.events)) async def _execute_diagnostic_requests(client): """Execute extended diagnostic requests.""" _logger.info("### Running diagnostic requests.") - rr = _check_call(await client.execute(req_diag.ReturnQueryDataRequest(unit=SLAVE))) + rr = _check_call(await client.execute(req_diag.ReturnQueryDataRequest(slave=SLAVE))) assert not rr.message[0] _check_call( - await client.execute(req_diag.RestartCommunicationsOptionRequest(unit=SLAVE)) + await client.execute(req_diag.RestartCommunicationsOptionRequest(slave=SLAVE)) ) _check_call( - await client.execute(req_diag.ReturnDiagnosticRegisterRequest(unit=SLAVE)) + await client.execute(req_diag.ReturnDiagnosticRegisterRequest(slave=SLAVE)) ) _check_call( - await client.execute(req_diag.ChangeAsciiInputDelimiterRequest(unit=SLAVE)) + await client.execute(req_diag.ChangeAsciiInputDelimiterRequest(slave=SLAVE)) ) - # NOT WORKING: _check_call(await client.execute(req_diag.ForceListenOnlyModeRequest(unit=SLAVE))) + # NOT WORKING: _check_call(await client.execute(req_diag.ForceListenOnlyModeRequest(slave=SLAVE))) # does not send a response _check_call(await client.execute(req_diag.ClearCountersRequest())) _check_call( await client.execute( - req_diag.ReturnBusCommunicationErrorCountRequest(unit=SLAVE) + req_diag.ReturnBusCommunicationErrorCountRequest(slave=SLAVE) ) ) _check_call( - await client.execute(req_diag.ReturnBusExceptionErrorCountRequest(unit=SLAVE)) + await client.execute(req_diag.ReturnBusExceptionErrorCountRequest(slave=SLAVE)) ) _check_call( - await client.execute(req_diag.ReturnSlaveMessageCountRequest(unit=SLAVE)) + await client.execute(req_diag.ReturnSlaveMessageCountRequest(slave=SLAVE)) ) _check_call( - await client.execute(req_diag.ReturnSlaveNoResponseCountRequest(unit=SLAVE)) + await client.execute(req_diag.ReturnSlaveNoResponseCountRequest(slave=SLAVE)) ) - _check_call(await client.execute(req_diag.ReturnSlaveNAKCountRequest(unit=SLAVE))) - _check_call(await client.execute(req_diag.ReturnSlaveBusyCountRequest(unit=SLAVE))) + _check_call(await client.execute(req_diag.ReturnSlaveNAKCountRequest(slave=SLAVE))) + _check_call(await client.execute(req_diag.ReturnSlaveBusyCountRequest(slave=SLAVE))) _check_call( await client.execute( - req_diag.ReturnSlaveBusCharacterOverrunCountRequest(unit=SLAVE) + req_diag.ReturnSlaveBusCharacterOverrunCountRequest(slave=SLAVE) ) ) - _check_call(await client.execute(req_diag.ReturnIopOverrunCountRequest(unit=SLAVE))) - _check_call(await client.execute(req_diag.ClearOverrunCountRequest(unit=SLAVE))) - # NOT WORKING _check_call(await client.execute(req_diag.GetClearModbusPlusRequest(unit=SLAVE))) + _check_call( + await client.execute(req_diag.ReturnIopOverrunCountRequest(slave=SLAVE)) + ) + _check_call(await client.execute(req_diag.ClearOverrunCountRequest(slave=SLAVE))) + # NOT WORKING _check_call(await client.execute(req_diag.GetClearModbusPlusRequest(slave=SLAVE))) # ------------------------ @@ -284,12 +289,15 @@ def run_sync_calls(client): template_call(client) +async def helper(): + """Combine the setup and run""" + testclient = setup_async_client(description="Run asynchronous client.") + await run_async_client(testclient, modbus_calls=run_async_calls) + + if __name__ == "__main__": - cmd_args = get_commandline( - server=False, - description="Run modbus calls in asynchronous client.", + asyncio.run(helper()) + testclient = setup_sync_client( + description="Run modbus calls in synchronous client." ) - testclient = setup_async_client(cmd_args) - asyncio.run(run_async_client(testclient, modbus_calls=run_async_calls)) - testclient = setup_sync_client(cmd_args) run_sync_client(testclient, modbus_calls=run_sync_calls) diff --git a/examples/v2.5.3/custom_message.py b/examples/client_custom_msg.py similarity index 90% rename from examples/v2.5.3/custom_message.py rename to examples/client_custom_msg.py index 661c37165..53b41c300 100755 --- a/examples/v2.5.3/custom_message.py +++ b/examples/client_custom_msg.py @@ -124,9 +124,25 @@ def __init__(self, address, **kwargs): # --------------------------------------------------------------------------- # -if __name__ == "__main__": +def run_custom_client(): + """Run versions of read coil.""" with ModbusClient(host="localhost", port=5020) as client: + # Standard call + request = ReadCoilsRequest(32, 1, slave=1) + result = client.execute(request) + print(result) + + # inherited request + request = Read16CoilsRequest(32, slave=1) + result = client.execute(request) + print(result) + + # new modbus function code. client.register(CustomModbusResponse) - request = CustomModbusRequest(1, unit=1) + request = CustomModbusRequest(32, slave=1) result = client.execute(request) - print(result.values) + print(result) + + +if __name__ == "__main__": + run_custom_client() diff --git a/examples/client_payload.py b/examples/client_payload.py index 5c71b762b..9d0073e2f 100755 --- a/examples/client_payload.py +++ b/examples/client_payload.py @@ -12,7 +12,6 @@ from collections import OrderedDict from examples.client_async import run_async_client, setup_async_client -from examples.helper import get_commandline from pymodbus.constants import Endian from pymodbus.payload import BinaryPayloadBuilder, BinaryPayloadDecoder @@ -137,9 +136,11 @@ async def run_payload_calls(client): print("\n") +async def helper(): + """Combine the setup and run""" + testclient = setup_async_client(description="Run asynchronous client.") + await run_async_client(testclient, modbus_calls=run_payload_calls) + + if __name__ == "__main__": - cmd_args = get_commandline( - description="Run payload client.", - ) - testclient = setup_async_client(cmd_args) - asyncio.run(run_async_client(testclient, modbus_calls=run_payload_calls)) + asyncio.run(helper()) diff --git a/examples/client_sync.py b/examples/client_sync.py index 0c71f89c8..2525b3188 100755 --- a/examples/client_sync.py +++ b/examples/client_sync.py @@ -39,8 +39,13 @@ _logger = logging.getLogger() -def setup_sync_client(args): +def setup_sync_client(description=None, cmdline=None): """Run client setup.""" + args = get_commandline( + server=False, + description=description, + cmdline=cmdline, + ) _logger.info("### Create client object") if args.comm == "tcp": client = ModbusTcpClient( @@ -126,9 +131,5 @@ def run_sync_client(client, modbus_calls=None): if __name__ == "__main__": - cmd_args = get_commandline( - server=False, - description="Run synchronous client.", - ) - testclient = setup_sync_client(cmd_args) + testclient = setup_sync_client(description="Run synchronous client.") run_sync_client(testclient) diff --git a/examples/client_sync_test.py b/examples/client_sync_test.py new file mode 100755 index 000000000..b75d68e2b --- /dev/null +++ b/examples/client_sync_test.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""Pymodbus Synchronous Client Example. + +An example of a single threaded synchronous client. + +usage: client_sync.py [-h] [--comm {tcp,udp,serial,tls}] + [--framer {ascii,binary,rtu,socket,tls}] + [--log {critical,error,warning,info,debug}] + [--port PORT] +options: + -h, --help show this help message and exit + --comm {tcp,udp,serial,tls} + "serial", "tcp", "udp" or "tls" + --framer {ascii,binary,rtu,socket,tls} + "ascii", "binary", "rtu", "socket" or "tls" + --log {critical,error,warning,info,debug} + "critical", "error", "warning", "info" or "debug" + --port PORT the port to use + --baudrate BAUDRATE the baud rate to use for the serial device + +The corresponding server must be started before e.g. as: + python3 server_sync.py +""" +import logging + +import pymodbus.bit_read_message as pdu_bit_read +import pymodbus.bit_write_message as pdu_bit_write + +# --------------------------------------------------------------------------- # +# import the various client implementations +# --------------------------------------------------------------------------- # +from examples.helper import get_commandline +from pymodbus.client import ModbusTcpClient +from pymodbus.exceptions import ModbusException + + +_logger = logging.getLogger() + + +def run_sync_client(): + """Run sync client.""" + args = get_commandline() + _logger.info("### Create client object") + client = ModbusTcpClient( + args.host, + port=args.port, + framer=args.framer, + ) + + _logger.info("### Client starting") + client.connect() + + _logger.info("### first read_coils to secure it works generally") + try: + rr = client.read_coils(1, 1, slave=1) + except ModbusException as exc: + _logger.error(exc) + raise RuntimeError(exc) from exc + if rr.isError(): + raise RuntimeError("ERROR: read_coils returned an error!") + assert isinstance(rr, pdu_bit_read.ReadCoilsResponse) + + _logger.info("### next write_coil to change a value") + try: + rr = client.write_coil(1, 17, slave=1) + except ModbusException as exc: + _logger.error(exc) + raise RuntimeError(exc) from exc + if rr.isError(): + raise RuntimeError("ERROR: write_coil returned an error!") + assert isinstance(rr, pdu_bit_write.WriteSingleCoilResponse) + + _logger.info("### finally read_coil to verify value") + try: + rr = client.read_coils(1, 1, slave=1) + except ModbusException as exc: + _logger.error(exc) + raise RuntimeError(exc) from exc + if rr.isError(): + raise RuntimeError("ERROR: read_coils(2) returned an error!") + assert isinstance(rr, pdu_bit_read.ReadCoilsResponse) + + client.close() + _logger.info("### End of Program") + + +if __name__ == "__main__": + run_sync_client() diff --git a/examples/client_test.py b/examples/client_test.py index 8c6575c15..b56e8f1b6 100755 --- a/examples/client_test.py +++ b/examples/client_test.py @@ -13,7 +13,6 @@ import pymodbus.diag_message as req_diag import pymodbus.mei_message as req_mei from examples.client_async import run_async_client, setup_async_client -from examples.helper import get_commandline from pymodbus.pdu import ExceptionResponse @@ -50,53 +49,59 @@ async def _handle_discrete_input(client): async def _handle_holding_registers(client): """Read/write holding registers.""" _logger.info("### write holding register and read holding registers") - _check_call(await client.write_register(3, 17, slave=SLAVE)) + _check_call(await client.write_register(3, 21, slave=SLAVE)) rr = None - rr = _check_call(await client.read_holding_registers(3, 1, slave=SLAVE)) - assert rr.registers[0] == 17 rr = _check_call(await client.read_holding_registers(4, 2, slave=SLAVE)) - assert rr.registers[0] == 9 - assert rr.registers[1] == 27177 + assert rr.registers[0] == 17 + assert rr.registers[1] == 17 + rr = _check_call(await client.read_holding_registers(3, 1, slave=SLAVE)) + assert rr.registers[0] == 21 async def _execute_information_requests(client): """Execute extended information requests.""" _logger.info("### Running information requests.") rr = _check_call( - await client.execute(req_mei.ReadDeviceInformationRequest(unit=SLAVE)) + await client.execute(req_mei.ReadDeviceInformationRequest(slave=SLAVE)) ) - assert rr.information[0] == b"pymodbus" + assert rr.information[0] == b"Pymodbus" async def _execute_diagnostic_requests(client): """Execute extended diagnostic requests.""" _logger.info("### Running diagnostic requests.") - rr = _check_call(await client.execute(req_diag.ReturnQueryDataRequest(unit=SLAVE))) + rr = _check_call(await client.execute(req_diag.ReturnQueryDataRequest(slave=SLAVE))) assert not rr.message[0] _check_call( - await client.execute(req_diag.RestartCommunicationsOptionRequest(unit=SLAVE)) + await client.execute(req_diag.RestartCommunicationsOptionRequest(slave=SLAVE)) ) # ------------------------ # Run the calls in groups. # ------------------------ - - -async def run_async_calls(client): +async def run_async_simple_calls(client): """Demonstrate basic read/write calls.""" await _handle_coils(client) await _handle_discrete_input(client) await _handle_holding_registers(client) + + +async def run_async_extended_calls(client): + """Demonstrate basic read/write calls.""" await _execute_information_requests(client) await _execute_diagnostic_requests(client) +async def run_async_calls(client): + """Demonstrate basic read/write calls.""" + await run_async_simple_calls(client) + await run_async_extended_calls(client) + + if __name__ == "__main__": - cmd_args = get_commandline( - server=False, - description="Run modbus calls in asynchronous client.", + testclient = setup_async_client( + description="Run modbus calls in asynchronous client." ) - testclient = setup_async_client(cmd_args) asyncio.run(run_async_client(testclient, modbus_calls=run_async_calls)) diff --git a/pymodbus/datastore/database/redis_datastore.py b/examples/contrib/redis_datastore.py similarity index 99% rename from pymodbus/datastore/database/redis_datastore.py rename to examples/contrib/redis_datastore.py index 73801212a..14d96d633 100644 --- a/pymodbus/datastore/database/redis_datastore.py +++ b/examples/contrib/redis_datastore.py @@ -1,11 +1,10 @@ """Datastore using redis.""" # pylint: disable=missing-type-doc +from contextlib import suppress -try: +with suppress(ImportError): import redis -except ImportError: - pass from pymodbus.datastore import ModbusBaseSlaveContext from pymodbus.logging import Log diff --git a/examples/contrib/serial_forwarder.py b/examples/contrib/serial_forwarder.py index a97506c96..b29fd824d 100644 --- a/examples/contrib/serial_forwarder.py +++ b/examples/contrib/serial_forwarder.py @@ -14,8 +14,6 @@ from pymodbus.server.async_io import ModbusTcpServer -FORMAT = "%(asctime)-15s %(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s" -logging.basicConfig(format=FORMAT) _logger = logging.getLogger() @@ -40,7 +38,7 @@ async def run(self): _logger.info(message) store = {} for i in slaves: - store[i] = RemoteSlaveContext(client, unit=i) + store[i] = RemoteSlaveContext(client, slave=i) context = ModbusServerContext(slaves=store, single=False) self.server = ModbusTcpServer( context, address=(server_ip, server_port), allow_reuse_address=True @@ -60,10 +58,14 @@ async def stop(self): def get_commandline(): """Read and validate command line arguments""" - logchoices = ["critical", "error", "warning", "info", "debug"] - parser = argparse.ArgumentParser(description="Command line options") - parser.add_argument("--log", help=",".join(logchoices), default="info", type=str) + parser.add_argument( + "--log", + choices=["critical", "error", "warning", "info", "debug"], + help="set log level, default is info", + default="info", + type=str, + ) parser.add_argument( "--port", help="RTU serial port", default="/dev/ttyUSB0", type=str ) @@ -77,9 +79,7 @@ def get_commandline(): args = parser.parse_args() # set defaults - _logger.setLevel( - args.log.upper() if args.log.lower() in logchoices else logging.INFO - ) + _logger.setLevel(args.log.upper()) if not args.slaves: args.slaves = {1, 2, 3} return args.port, args.baudrate, args.server_port, args.server_ip, args.slaves diff --git a/pymodbus/datastore/database/sql_datastore.py b/examples/contrib/sql_datastore.py similarity index 100% rename from pymodbus/datastore/database/sql_datastore.py rename to examples/contrib/sql_datastore.py diff --git a/examples/helper.py b/examples/helper.py index b1d703458..56296003c 100755 --- a/examples/helper.py +++ b/examples/helper.py @@ -5,9 +5,7 @@ get_command_line """ import argparse -import dataclasses import logging -from dataclasses import dataclass from pymodbus import pymodbus_apply_logging_config from pymodbus.transaction import ( @@ -22,58 +20,40 @@ _logger = logging.getLogger() -@dataclass -class Commandline: - """Simulate commandline parameters. - - Replaces get_commandline() and allows application to set arguments directly. - """ - - comm = None - framer = None - host = "127.0.0.1" - port = None - baudrate = 9600 - store = "sequential" - identity = None - context = None - slaves = None - client_port = None - client = None - - @classmethod - def copy(cls): - """Copy Commandline""" - to_copy = cls() - return dataclasses.replace(to_copy) - - -def get_commandline(server=False, description=None, extras=None): +def get_commandline(server=False, description=None, extras=None, cmdline=None): """Read and validate command line arguments""" parser = argparse.ArgumentParser(description=description) parser.add_argument( + "-c", "--comm", choices=["tcp", "udp", "serial", "tls"], help="set communication, default is tcp", + dest="comm", default="tcp", type=str, ) parser.add_argument( + "-f", "--framer", choices=["ascii", "binary", "rtu", "socket", "tls"], help="set framer, default depends on --comm", + dest="framer", 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( + "-p", "--port", help="set port", + dest="port", type=str, ) parser.add_argument( @@ -106,14 +86,14 @@ def get_commandline(server=False, description=None, extras=None): parser.add_argument( "--host", help="set host, default is 127.0.0.1", + dest="host", default="127.0.0.1", type=str, ) - if extras: for extra in extras: parser.add_argument(extra[0], **extra[1]) - args = parser.parse_args() + args = parser.parse_args(cmdline) # set defaults comm_defaults = { @@ -129,7 +109,7 @@ def get_commandline(server=False, description=None, extras=None): "socket": ModbusSocketFramer, "tls": ModbusTlsFramer, } - pymodbus_apply_logging_config() + pymodbus_apply_logging_config(args.log) _logger.setLevel(args.log.upper()) args.framer = framers[args.framer or comm_defaults[args.comm][0]] args.port = args.port or comm_defaults[args.comm][1] diff --git a/examples/message_generator.py b/examples/message_generator.py new file mode 100755 index 000000000..82731c14d --- /dev/null +++ b/examples/message_generator.py @@ -0,0 +1,218 @@ +#!/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() + + +# -------------------------------------------------------------------------- # +# 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 new file mode 100755 index 000000000..f24ee4bd0 --- /dev/null +++ b/examples/message_parser.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +"""Modbus Message Parser. + +The following is an example of how to parse modbus messages +using the supplied framers. + +""" +import argparse +import codecs as c +import collections +import logging +import textwrap + +from pymodbus.factory import ClientDecoder, ServerDecoder +from pymodbus.transaction import ( + ModbusAsciiFramer, + ModbusBinaryFramer, + ModbusRtuFramer, + ModbusSocketFramer, +) + + +_logger = logging.getLogger() + + +def get_commandline(cmdline): + """Parse the command line options""" + parser = argparse.ArgumentParser() + + parser.add_argument( + "--framer", + choices=["ascii", "binary", "rtu", "socket"], + help="set framer, default is rtu", + type=str, + default="rtu", + dest="framer", + ) + parser.add_argument( + "-l", + "--log", + choices=["critical", "error", "warning", "info", "debug"], + help="set log level, default is info", + default="info", + type=str, + dest="log", + ) + parser.add_argument( + "-m", + "--message", + help="The message to parse", + type=str, + default=None, + dest="message", + ) + return parser.parse_args(cmdline) + + +class Decoder: + """Decoder. + + build custom wrapper around the framers + """ + + def __init__(self, framer, encode=False): + """Initialize a new instance of the decoder""" + self.framer = framer + self.encode = encode + + def decode(self, message): + """Attempt to decode the supplied message""" + value = message if self.encode else c.encode(message, "hex_codec") + print("=" * 80) + print(f"Decoding Message {value}") + print("=" * 80) + decoders = [ + self.framer(ServerDecoder(), client=None), + self.framer(ClientDecoder(), client=None), + ] + for decoder in decoders: + print(f"{decoder.decoder.__class__.__name__}") + print("-" * 80) + try: + decoder.addToFrame(message) + if decoder.checkFrame(): + slave = decoder._header.get( # pylint: disable=protected-access + "uid", 0x00 + ) + decoder.advanceFrame() + decoder.processIncomingPacket(message, self.report, slave) + else: + self.check_errors(decoder, message) + except Exception: # pylint: disable=broad-except + self.check_errors(decoder, message) + + def check_errors(self, decoder, message): + """Attempt to find message errors""" + txt = f"Unable to parse message - {message} with {decoder}" + _logger.error(txt) + + def report(self, message): + """Print the message information""" + print( + "%-15s = %s" # pylint: disable=consider-using-f-string + % ( + "name", + message.__class__.__name__, + ) + ) + for k_dict, v_dict in message.__dict__.items(): + if isinstance(v_dict, dict): + print("%-15s =" % k_dict) # pylint: disable=consider-using-f-string + for k_item, v_item in v_dict.items(): + print( + " %-12s => %s" # pylint: disable=consider-using-f-string + % (k_item, v_item) + ) + elif isinstance(v_dict, collections.abc.Iterable): + print("%-15s =" % k_dict) # pylint: disable=consider-using-f-string + value = str([int(x) for x in v_dict]) + for line in textwrap.wrap(value, 60): + print( + "%-15s . %s" # pylint: disable=consider-using-f-string + % ("", line) + ) + else: + print( + "%-15s = %s" # pylint: disable=consider-using-f-string + % (k_dict, hex(v_dict)) + ) + print( + "%-15s = %s" # pylint: disable=consider-using-f-string + % ( + "documentation", + message.__doc__, + ) + ) + + +# -------------------------------------------------------------------------- # +# and decode our message +# -------------------------------------------------------------------------- # + + +def parse_messages(cmdline=None): + """Do a helper method to generate the messages to parse""" + args = get_commandline(cmdline=cmdline) + _logger.setLevel(args.log.upper()) + if not args.message: + _logger.error("Missing --message.") + return + + framer = { + "ascii": ModbusAsciiFramer, + "binary": ModbusBinaryFramer, + "rtu": ModbusRtuFramer, + "socket": ModbusSocketFramer, + }[args.framer] + decoder = Decoder(framer) + + raw_message = c.decode(args.message.encode(), "hex_codec") + decoder.decode(raw_message) + + +if __name__ == "__main__": + parse_messages() diff --git a/examples/modbus_forwarder.py b/examples/modbus_forwarder.py index 5a015108a..28a94720a 100755 --- a/examples/modbus_forwarder.py +++ b/examples/modbus_forwarder.py @@ -14,7 +14,7 @@ Both server and client are tcp based, but it can be easily modified to any server/client (see client_sync.py and server_sync.py for other communication types) -**WARNING** THIS EXAMPLE IS KNOWN TO HAVE PROBLEMS, a wrong solution. +**WARNING** This example is a simple solution, that do only forward data (read/write) requests. """ import asyncio import logging @@ -46,16 +46,16 @@ async def run_forwarder(args): ) await args.client.connect() assert args.client.connected - # If required to communicate with a specified client use unit= + # If required to communicate with a specified client use slave= # in RemoteSlaveContext - # For e.g to forward the requests to slave with unit address 1 use - # store = RemoteSlaveContext(client, unit=1) + # For e.g to forward the requests to slave with slave address 1 use + # store = RemoteSlaveContext(client, slave=1) if args.slaves: store = {} for i in args.slaves: - store[i.to_bytes(1, "big")] = RemoteSlaveContext(args.client, unit=i) + store[i.to_bytes(1, "big")] = RemoteSlaveContext(args.client, slave=i) else: - store = RemoteSlaveContext(args.client, unit=1) + store = RemoteSlaveContext(args.client, slave=1) args.context = ModbusServerContext(slaves=store, single=True) await StartAsyncTcpServer(context=args.context, address=("localhost", args.port)) diff --git a/examples/server_async.py b/examples/server_async.py index 5dd62f3c5..291aa6eb0 100755 --- a/examples/server_async.py +++ b/examples/server_async.py @@ -56,14 +56,17 @@ _logger = logging.getLogger() -def setup_server(args): +def setup_server(description=None, context=None, cmdline=None): """Run server setup.""" - # The datastores only respond to the addresses that are initialized - # If you initialize a DataBlock to addresses of 0x00 to 0xFF, a request to - # 0x100 will respond with an invalid address exception. - # This is because many devices exhibit this kind of behavior (but not all) + args = get_commandline(server=True, description=description, cmdline=cmdline) + if context: + args.context = context if not args.context: _logger.info("### Create datastore") + # The datastores only respond to the addresses that are initialized + # If you initialize a DataBlock to addresses of 0x00 to 0xFF, a request to + # 0x100 will respond with an invalid address exception. + # This is because many devices exhibit this kind of behavior (but not all) if args.store == "sequential": # Continuing, use a sequential block without gaps. datablock = ModbusSequentialDataBlock(0x00, [17] * 100) @@ -78,11 +81,11 @@ def setup_server(args): if args.slaves: # The server then makes use of a server context that allows the server - # to respond with different slave contexts for different unit ids. - # By default it will return the same context for every unit id supplied + # to respond with different slave contexts for different slave ids. + # By default it will return the same context for every slave id supplied # (broadcast mode). # However, this can be overloaded by setting the single flag to False and - # then supplying a dictionary of unit id to context mapping:: + # then supplying a dictionary of slave id to context mapping:: # # The slave context can also be initialized in zero_mode which means # that a request to address(0-7) will map to the address (0-7). @@ -112,7 +115,7 @@ def setup_server(args): single = False else: context = ModbusSlaveContext( - di=datablock, co=datablock, hr=datablock, ir=datablock, unit=1 + di=datablock, co=datablock, hr=datablock, ir=datablock ) single = True @@ -154,10 +157,9 @@ async def run_async_server(args): # handler=None, # handler for each session allow_reuse_address=True, # allow the reuse of an address # ignore_missing_slaves=True, # ignore request to a missing slave - # broadcast_enable=False, # treat unit_id 0 as broadcast address, + # broadcast_enable=False, # treat slave_id 0 as broadcast address, # timeout=1, # waiting time for request to complete # TBD strict=True, # use strict timing, t1.5 for Modbus RTU - # defer_start=False, # Only define server do not activate ) elif args.comm == "udp": address = ("127.0.0.1", args.port) if args.port else None @@ -169,10 +171,9 @@ async def run_async_server(args): framer=args.framer, # The framer strategy to use # handler=None, # handler for each session # ignore_missing_slaves=True, # ignore request to a missing slave - # broadcast_enable=False, # treat unit_id 0 as broadcast address, + # broadcast_enable=False, # treat slave_id 0 as broadcast address, # timeout=1, # waiting time for request to complete # TBD strict=True, # use strict timing, t1.5 for Modbus RTU - # defer_start=False, # Only define server do not activate ) elif args.comm == "serial": # socat -d -d PTY,link=/tmp/ptyp0,raw,echo=0,ispeed=9600 @@ -191,9 +192,8 @@ async def run_async_server(args): baudrate=args.baudrate, # The baud rate to use for the serial device # 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 unit_id 0 as broadcast address, + # broadcast_enable=False, # treat slave_id 0 as broadcast address, # strict=True, # use strict timing, t1.5 for Modbus RTU - # defer_start=False, # Only define server do not activate ) elif args.comm == "tls": address = ("", args.port) if args.port else None @@ -220,18 +220,13 @@ async def run_async_server(args): # password="none", # The password for for decrypting the private key file # reqclicert=False, # Force the sever request client"s certificate # ignore_missing_slaves=True, # ignore request to a missing slave - # broadcast_enable=False, # treat unit_id 0 as broadcast address, + # broadcast_enable=False, # treat slave_id 0 as broadcast address, # timeout=1, # waiting time for request to complete # TBD strict=True, # use strict timing, t1.5 for Modbus RTU - defer_start=False, # Only define server do not activate ) return server if __name__ == "__main__": - cmd_args = get_commandline( - server=True, - description="Run asynchronous server.", - ) - run_args = setup_server(cmd_args) + run_args = setup_server(description="Run asynchronous server.") asyncio.run(run_async_server(run_args), debug=True) diff --git a/examples/server_callback.py b/examples/server_callback.py new file mode 100755 index 000000000..582047beb --- /dev/null +++ b/examples/server_callback.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +"""Pymodbus Server With Callbacks. + +This is an example of adding callbacks to a running modbus server +when a value is written to it. +""" +import asyncio +import logging + +from examples.server_async import run_async_server, setup_server +from pymodbus.datastore import ( + ModbusSequentialDataBlock, + ModbusServerContext, + ModbusSlaveContext, +) + + +_logger = logging.getLogger() +_logger.setLevel(logging.DEBUG) + + +class CallbackDataBlock(ModbusSequentialDataBlock): + """A datablock that stores the new value in memory, + + and passes the operation to a message queue for further processing. + """ + + def __init__(self, queue, addr, values): + """Initialize.""" + self.queue = queue + super().__init__(addr, values) + + def setValues(self, address, value): + """Set the requested values of the datastore.""" + super().setValues(address, value) + txt = f"Callback from setValues with address {address}, value {value}" + _logger.debug(txt) + + def getValues(self, address, count=1): + """Return the requested values from the datastore.""" + result = super().getValues(address, count=count) + txt = f"Callback from getValues with address {address}, count {count}, data {result}" + _logger.debug(txt) + return result + + def validate(self, address, count=1): + """Check to see if the request is in range.""" + result = super().validate(address, count=count) + txt = f"Callback from validate with address {address}, count {count}, data {result}" + _logger.debug(txt) + return result + + +async def run_callback_server(cmdline=None): + """Define datastore callback for server and do setup.""" + queue = asyncio.Queue() + block = CallbackDataBlock(queue, 0x00, [17] * 100) + store = ModbusSlaveContext(di=block, co=block, hr=block, ir=block) + context = ModbusServerContext(slaves=store, single=True) + run_args = setup_server( + description="Run callback server.", cmdline=cmdline, context=context + ) + await run_async_server(run_args) + + +if __name__ == "__main__": + asyncio.run(run_callback_server()) diff --git a/examples/server_payload.py b/examples/server_payload.py index 5493fbd56..e047ba290 100755 --- a/examples/server_payload.py +++ b/examples/server_payload.py @@ -7,9 +7,7 @@ import asyncio import logging -from examples.helper import get_commandline from examples.server_async import run_async_server, setup_server -from pymodbus import pymodbus_apply_logging_config from pymodbus.constants import Endian from pymodbus.datastore import ( ModbusSequentialDataBlock, @@ -22,12 +20,9 @@ _logger = logging.getLogger() -def setup_payload_server(args): +def setup_payload_server(cmdline=None): """Define payload for server and do setup.""" - pymodbus_apply_logging_config() - _logger.setLevel(logging.DEBUG) - # ----------------------------------------------------------------------- # # build your payload # ----------------------------------------------------------------------- # @@ -57,14 +52,12 @@ def setup_payload_server(args): block = ModbusSequentialDataBlock(1, builder.to_registers()) store = ModbusSlaveContext(di=block, co=block, hr=block, ir=block) - args.context = ModbusServerContext(slaves=store, single=True) - return setup_server(args) + context = ModbusServerContext(slaves=store, single=True) + return setup_server( + description="Run payload server.", cmdline=cmdline, context=context + ) if __name__ == "__main__": - cmd_args = get_commandline( - server=True, - description="Run payload server.", - ) - run_args = setup_payload_server(cmd_args) + run_args = setup_payload_server() asyncio.run(run_async_server(run_args)) diff --git a/examples/server_simulator.py b/examples/server_simulator.py index 6608cc612..53dc13561 100755 --- a/examples/server_simulator.py +++ b/examples/server_simulator.py @@ -24,11 +24,12 @@ from pymodbus import pymodbus_apply_logging_config from pymodbus.datastore import ModbusServerContext, ModbusSimulatorContext +from pymodbus.device import ModbusDeviceIdentification from pymodbus.server import StartAsyncTcpServer from pymodbus.transaction import ModbusSocketFramer -_logger = logging.getLogger() +_logger = logging.getLogger(__name__) demo_config = { "setup": { @@ -57,18 +58,21 @@ }, "invalid": [ 1, - [3, 4], + [6, 6], ], "write": [ - 5, + 3, [7, 8], [16, 18], [21, 26], [31, 36], ], "bits": [ - 5, - [7, 8], + [7, 9], + {"addr": 2, "value": 0x81}, + {"addr": 3, "value": 17}, + {"addr": 4, "value": 17}, + {"addr": 5, "value": 17}, {"addr": 10, "value": 0x81}, {"addr": [11, 12], "value": 0x04342}, {"addr": 13, "action": "reset"}, @@ -111,7 +115,7 @@ def custom_action2(_inx, _cell): } -def get_commandline(): +def get_commandline(cmdline=None): """Read and validate command line arguments""" parser = argparse.ArgumentParser(description="Run server simulator.") parser.add_argument( @@ -122,17 +126,19 @@ def get_commandline(): type=str, ) parser.add_argument("--port", help="set port", type=str, default="5020") - args = parser.parse_args() + args = parser.parse_args(cmdline) - pymodbus_apply_logging_config() - _logger.setLevel(args.log.upper()) - args.framer = ModbusSocketFramer - args.port = int(args.port) return args -def setup_simulator(args, setup=None, actions=None): +def setup_simulator(setup=None, actions=None, cmdline=None): """Run server setup.""" + args = get_commandline(cmdline=cmdline) + pymodbus_apply_logging_config(args.log) + _logger.setLevel(args.log.upper()) + args.framer = ModbusSocketFramer + args.port = int(args.port) + _logger.info("### Create datastore") if not setup: setup = demo_config @@ -140,12 +146,24 @@ def setup_simulator(args, setup=None, actions=None): actions = demo_actions context = ModbusSimulatorContext(setup, actions) args.context = ModbusServerContext(slaves=context, single=True) + args.identity = ModbusDeviceIdentification( + info_name={ + "VendorName": "Pymodbus", + "ProductCode": "PM", + "VendorUrl": "https://github.com/pymodbus-dev/pymodbus/", + "ProductName": "Pymodbus Server", + "ModelName": "Pymodbus Server", + "MajorMinorRevision": "test", + } + ) return args async def run_server_simulator(args): """Run server.""" _logger.info("### start server simulator") + + pymodbus_apply_logging_config(args.log.upper()) await StartAsyncTcpServer( context=args.context, address=("", args.port), @@ -155,6 +173,5 @@ async def run_server_simulator(args): if __name__ == "__main__": - cmd_args = get_commandline() - run_args = setup_simulator(cmd_args) + run_args = setup_simulator() asyncio.run(run_server_simulator(run_args), debug=True) diff --git a/examples/server_sync.py b/examples/server_sync.py index 979e658d0..ac21f6490 100755 --- a/examples/server_sync.py +++ b/examples/server_sync.py @@ -35,7 +35,6 @@ import logging import os -from examples.helper import get_commandline from examples.server_async import setup_server # --------------------------------------------------------------------------- # @@ -69,10 +68,9 @@ def run_sync_server(args): # TBD handler=None, # handler for each session allow_reuse_address=True, # allow the reuse of an address # ignore_missing_slaves=True, # ignore request to a missing slave - # broadcast_enable=False, # treat unit_id 0 as broadcast address, + # broadcast_enable=False, # treat slave_id 0 as broadcast address, # timeout=1, # waiting time for request to complete # TBD strict=True, # use strict timing, t1.5 for Modbus RTU - # defer_start=False, # Only define server do not activate ) elif args.comm == "udp": address = ("", args.port) if args.port else None @@ -86,10 +84,9 @@ def run_sync_server(args): framer=args.framer, # The framer strategy to use # TBD handler=None, # handler for each session # ignore_missing_slaves=True, # ignore request to a missing slave - # broadcast_enable=False, # treat unit_id 0 as broadcast address, + # broadcast_enable=False, # treat slave_id 0 as broadcast address, # timeout=1, # waiting time for request to complete # TBD strict=True, # use strict timing, t1.5 for Modbus RTU - # defer_start=False, # Only define server do not activate ) elif args.comm == "serial": # socat -d -d PTY,link=/tmp/ptyp0,raw,echo=0,ispeed=9600 @@ -108,9 +105,8 @@ def run_sync_server(args): baudrate=args.baudrate, # The baud rate to use for the serial device # 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 unit_id 0 as broadcast address, + # broadcast_enable=False, # treat slave_id 0 as broadcast address, # strict=True, # use strict timing, t1.5 for Modbus RTU - # defer_start=False, # Only define server do not activate ) elif args.comm == "tls": address = ("", args.port) if args.port else None @@ -137,19 +133,14 @@ def run_sync_server(args): # password=None, # The password for for decrypting the private key file # reqclicert=False, # Force the sever request client"s certificate # ignore_missing_slaves=True, # ignore request to a missing slave - # broadcast_enable=False, # treat unit_id 0 as broadcast address, + # broadcast_enable=False, # treat slave_id 0 as broadcast address, # timeout=1, # waiting time for request to complete # TBD strict=True, # use strict timing, t1.5 for Modbus RTU - # defer_start=False, # Only define server do not activate ) return server if __name__ == "__main__": - cmd_args = get_commandline( - server=True, - description="Run synchronous server.", - ) - run_args = setup_server(cmd_args) + run_args = setup_server(description="Run synchronous server.") server = run_sync_server(run_args) server.shutdown() diff --git a/examples/server_updating.py b/examples/server_updating.py index c7cc2b5c0..dc45b3d8e 100755 --- a/examples/server_updating.py +++ b/examples/server_updating.py @@ -30,7 +30,6 @@ import asyncio import logging -from examples.helper import get_commandline from examples.server_async import run_async_server, setup_server from pymodbus.datastore import ( ModbusSequentialDataBlock, @@ -43,10 +42,9 @@ async def updating_task(context): - """Run every so often, + """Run every so often and update live values of the context. - and updates live values of the context. It should be noted - that there is a lrace condition for the update. + It should be noted that there is a race condition for the update. """ _logger.debug("updating the context") fc_as_hex = 3 @@ -60,7 +58,7 @@ async def updating_task(context): await asyncio.sleep(1) -def setup_updating_server(args): +def setup_updating_server(cmdline=None): """Run server setup.""" # The datastores only respond to the addresses that are initialized # If you initialize a DataBlock to addresses of 0x00 to 0xFF, a request to @@ -69,23 +67,19 @@ def setup_updating_server(args): # Continuing, use a sequential block without gaps. datablock = ModbusSequentialDataBlock(0x00, [17] * 100) - context = ModbusSlaveContext( - di=datablock, co=datablock, hr=datablock, ir=datablock, unit=1 + context = ModbusSlaveContext(di=datablock, co=datablock, hr=datablock, ir=datablock) + context = ModbusServerContext(slaves=context, single=True) + return setup_server( + description="Run asynchronous server.", context=context, cmdline=cmdline ) - args.context = ModbusServerContext(slaves=context, single=True) - return setup_server(args) async def run_updating_server(args): """Start updater task and async server.""" - asyncio.create_task(updating_task(args.context)) + asyncio.create_task(updating_task(args.context)) # noqa: RUF006 await run_async_server(args) if __name__ == "__main__": - cmd_args = get_commandline( - server=True, - description="Run asynchronous server.", - ) - run_args = setup_updating_server(cmd_args) + run_args = setup_updating_server() asyncio.run(run_updating_server(run_args), debug=True) diff --git a/examples/v2.5.3/README.rst b/examples/v2.5.3/README.rst deleted file mode 100644 index 701ff94c5..000000000 --- a/examples/v2.5.3/README.rst +++ /dev/null @@ -1,81 +0,0 @@ -============================================================ -Modbus Implementations -============================================================ - -There are a few reference implementations that you can use -to test modbus serial - ------------------------------------------------------------- -pymodbus ------------------------------------------------------------- - -You can use pymodbus as a testing server by simply modifying -one of the run scripts supplied here. There is an -asynchronous version and a synchronous version (that really -differ in how many dependencies you are willing to have). -Regardless of which one you choose, they can be started -quite easily:: - - ./asynchronous-server.py - ./synchronous-server.py - -Currently, each version has implementations of the following: - -- modbus tcp -- modbus udp -- modbus udp binary -- modbus ascii serial -- modbus ascii rtu - -============================================================ -Serial Loopback Testing -============================================================ - -In order to test the serial implementations, one needs to -create a loopback connection (virtual serial port). This can -be done in a number of ways. - ------------------------------------------------------------- -Linux ------------------------------------------------------------- - -For linux, there are three ways that are included with this -distribution. - -One is to use the socat utility. The following will get one -going quickly:: - - sudo apt-get install socat - sudo socat PTY,link=/dev/pts/13, PTY,link=/dev/pts/14 - # connect the master to /dev/pts/13 - # connect the client to /dev/pts/14 - -Next, you can include the loopback kernel driver included in -the tools/nullmodem/linux directory:: - - sudo ./run - -The third method is Generic Unix method below. - ------------------------------------------------------------- -Windows ------------------------------------------------------------- - -For Windows, simply use the com0com application that is in -the directory tools/nullmodem/windows. Instructions are -included in the Readme.txt. - ------------------------------------------------------------- -Generic ------------------------------------------------------------- - -For most unix based systems, there is a simple virtual serial -forwarding application in the tools/nullmodem/ directory:: - - make run - # connect the master to the master output - # connect the client to the client output - -Or for a tried and true method, simply connect a null modem -cable between two of your serial ports and then simply reference -those. diff --git a/examples/v2.5.3/asynchronous_asyncio_modbus_tls_client.py b/examples/v2.5.3/asynchronous_asyncio_modbus_tls_client.py deleted file mode 100755 index 1d9ecd3e9..000000000 --- a/examples/v2.5.3/asynchronous_asyncio_modbus_tls_client.py +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env python3 -"""Simple Asynchronous Modbus TCP over TLS client. - -This is a simple example of writing a asynchronous modbus TCP over TLS client -that uses Python builtin module ssl - TLS/SSL wrapper for socket objects for -the TLS feature and asyncio. -""" -import asyncio - -# -------------------------------------------------------------------------- # -# import necessary libraries -# -------------------------------------------------------------------------- # -import ssl - -from pymodbus.client import AsyncModbusTlsClient - - -# -------------------------------------------------------------------------- # -# the TLS detail security can be set in SSLContext which is the context here -# -------------------------------------------------------------------------- # -sslctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) -sslctx.verify_mode = ssl.CERT_REQUIRED -sslctx.check_hostname = True - -# Prepare client's certificate which the server requires for TLS full handshake -# sslctx.load_cert_chain(certfile="client.crt", keyfile="client.key", -# password="pwd") - - -async def start_async_test(client): - """Start async test.""" - result = await client.read_coils(1, 8) - print(result.bits) - await client.write_coils(1, [False] * 3) - result = await client.read_coils(1, 8) - print(result.bits) - - -if __name__ == "__main__": - # ----------------------------------------------------------------------- # - # pass SSLContext which is the context here to ModbusTcpClient() - # ----------------------------------------------------------------------- # - new_client = AsyncModbusTlsClient( # pylint: disable=unpacking-non-sequence - "test.host.com", - 8020, - sslctx=sslctx, - ) - loop = asyncio.get_running_loop() - loop.run_until_complete(start_async_test(new_client.protocol)) - loop.close() diff --git a/examples/v2.5.3/callback_server.py b/examples/v2.5.3/callback_server.py deleted file mode 100755 index 87984dbac..000000000 --- a/examples/v2.5.3/callback_server.py +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env python3 -# pylint: disable=missing-type-doc,missing-param-doc,differing-param-doc -"""Pymodbus Server With Callbacks. - -This is an example of adding callbacks to a running modbus server -when a value is written to it. In order for this to work, it needs -a device-mapping file. -""" -import logging -from multiprocessing import Queue -from threading import Thread - -# --------------------------------------------------------------------------- # -# import the modbus libraries we need -# --------------------------------------------------------------------------- # -from pymodbus import __version__ as pymodbus_version -from pymodbus.datastore import ( - ModbusServerContext, - ModbusSlaveContext, - ModbusSparseDataBlock, -) -from pymodbus.device import ModbusDeviceIdentification -from pymodbus.server import StartTcpServer - - -# from pymodbus.transaction import ModbusRtuFramer, ModbusAsciiFramer - - -# --------------------------------------------------------------------------- # -# configure the service logging -# --------------------------------------------------------------------------- # -log = logging.getLogger() -log.setLevel(logging.DEBUG) - -# --------------------------------------------------------------------------- # -# create your custom data block with callbacks -# --------------------------------------------------------------------------- # - - -class CallbackDataBlock(ModbusSparseDataBlock): - """A datablock that stores the new value in memory, - - and passes the operation to a message queue for further processing. - """ - - def __init__(self, devices, queue): - """Initialize.""" - self.devices = devices - self.queue = queue - - values = {k: 0 for k in devices.keys()} - values[0xBEEF] = len(values) # the number of devices - super().__init__(values) - - def setValues(self, address, value): # pylint: disable=arguments-differ - """Set the requested values of the datastore - - :param address: The starting address - :param values: The new values to be set - """ - super().setValues(address, value) - self.queue.put((self.devices.get(address, None), value)) - - -# --------------------------------------------------------------------------- # -# define your callback process -# --------------------------------------------------------------------------- # - - -def rescale_value(value): - """Rescale the input value from the range of 0..100 to -3200..3200. - - :param value: The input value to scale - :returns: The rescaled value - """ - scale = 1 if value >= 50 else -1 - cur = value if value < 50 else (value - 50) - return scale * (cur * 64) - - -def device_writer(queue): - """Process new messages from a queue to write to device outputs - - :param queue: The queue to get new messages from - """ - while True: - device, value = queue.get() - rescale_value(value[0]) - txt = f"Write({device}) = {value}" - log.debug(txt) - if not device: - continue - # do any logic here to update your devices - - -# --------------------------------------------------------------------------- # -# initialize your device map -# --------------------------------------------------------------------------- # - - -def read_device_map(path): - """Read the device path to address mapping from file:: - - 0x0001,/dev/device1 - 0x0002,/dev/device2 - - :param path: The path to the input file - :returns: The input mapping file - """ - devices = {} - with open(path, "r") as stream: # pylint: disable=unspecified-encoding - for line in stream: - piece = line.strip().split(",") - devices[int(piece[0], 16)] = piece[1] - return devices - - -def run_callback_server(): - """Run callback server.""" - # ----------------------------------------------------------------------- # - # initialize your data store - # ----------------------------------------------------------------------- # - queue = Queue() - devices = read_device_map("device-mapping") - block = CallbackDataBlock(devices, queue) - store = ModbusSlaveContext(di=block, co=block, hr=block, ir=block) - context = ModbusServerContext(slaves=store, single=True) - - # ----------------------------------------------------------------------- # - # initialize the server information - # ----------------------------------------------------------------------- # - identity = ModbusDeviceIdentification( - info_name={ - "VendorName": "pymodbus", - "ProductCode": "PM", - "VendorUrl": "https://github.com/pymodbus-dev/pymodbus/", - "ProductName": "pymodbus Server", - "ModelName": "pymodbus Server", - "MajorMinorRevision": pymodbus_version, - } - ) - - # ----------------------------------------------------------------------- # - # run the server you want - # ----------------------------------------------------------------------- # - thread = Thread(target=device_writer, args=(queue,)) - thread.start() - StartTcpServer(context, identity=identity, address=("localhost", 5020)) - - -if __name__ == "__main__": - run_callback_server() diff --git a/examples/v2.5.3/changing_framers.py b/examples/v2.5.3/changing_framers.py deleted file mode 100755 index bf3f86fb3..000000000 --- a/examples/v2.5.3/changing_framers.py +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env python3 -"""Pymodbus Client Framer Overload. - -All of the modbus clients are designed to have pluggable framers -so that the transport and protocol are decoupled. This allows a user -to define or plug in their custom protocols into existing transports -(like a binary framer over a serial connection). - -It should be noted that although you are not limited to trying whatever -you would like, the library makes no guarantees that all framers with -all transports will produce predictable or correct results (for example -tcp transport with an RTU framer). However, please let us know of any -success cases that are not documented! -""" -import logging - -# --------------------------------------------------------------------------- # -# import the modbus client and the framers -# --------------------------------------------------------------------------- # -from pymodbus.client import ModbusTcpClient as ModbusClient -from pymodbus.transaction import ModbusSocketFramer as ModbusFramer - - -# from pymodbus.transaction import ModbusRtuFramer as ModbusFramer -# from pymodbus.transaction import ModbusBinaryFramer as ModbusFramer -# from pymodbus.transaction import ModbusAsciiFramer as ModbusFramer - -# --------------------------------------------------------------------------- # -# configure the client logging -# --------------------------------------------------------------------------- # -log = logging.getLogger() -log.setLevel(logging.DEBUG) - -if __name__ == "__main__": - # ----------------------------------------------------------------------- # - # Initialize the client - # ----------------------------------------------------------------------- # - client = ModbusClient("localhost", port=5020, framer=ModbusFramer) - client.connect() - - # ----------------------------------------------------------------------- # - # perform your requests - # ----------------------------------------------------------------------- # - rq = client.write_coil(1, True) - rr = client.read_coils(1, 1) - assert not rq.isError() # test that we are not an error - assert rr.bits[0] # test the expected value - - # ----------------------------------------------------------------------- # - # close the client - # ---------------------------------------------------------------------- # - client.close() diff --git a/examples/v2.5.3/concurrent_client.py b/examples/v2.5.3/concurrent_client.py deleted file mode 100755 index 43bb97c0e..000000000 --- a/examples/v2.5.3/concurrent_client.py +++ /dev/null @@ -1,259 +0,0 @@ -#!/usr/bin/env python3 -# pylint: disable=missing-type-doc,missing-param-doc,differing-param-doc -"""Concurrent Modbus Client. - -This is an example of writing a high performance modbus client that allows -a high level of concurrency by using worker threads/processes to handle -writing/reading from one or more client handles at once. -""" -import itertools - -# -------------------------------------------------------------------------- # -# import system libraries -# -------------------------------------------------------------------------- # -import logging -import multiprocessing -import threading -from collections import namedtuple -from concurrent.futures import Future -from multiprocessing import Event as mEvent -from multiprocessing import Process as mProcess -from multiprocessing import Queue as mQueue -from queue import Queue as qQueue -from threading import Event, Thread - -# -------------------------------------------------------------------------- # -# import necessary modbus libraries -# -------------------------------------------------------------------------- # -from pymodbus.client.mixin import ModbusClientMixin - - -# -------------------------------------------------------------------------- # -# configure the client logging -# -------------------------------------------------------------------------- # -log = logging.getLogger("pymodbus") -log.setLevel(logging.DEBUG) -logging.basicConfig() - - -# -------------------------------------------------------------------------- # -# Initialize out concurrency primitives -# -------------------------------------------------------------------------- # -class _Primitives: # pylint: disable=too-few-public-methods) - """This is a helper class. - - used to group the threading primitives depending on the type of - worker situation we want to run (threads or processes). - """ - - def __init__(self, **kwargs): - self.queue = kwargs.get("queue") - self.event = kwargs.get("event") - self.worker = kwargs.get("worker") - - @classmethod - def create(cls, in_process=False): - """Initialize a new instance of the concurrency primitives. - - :param in_process: True for threaded, False for processes - :returns: An initialized instance of concurrency primitives - """ - if in_process: - return cls(queue=qQueue, event=Event, worker=Thread) - return cls(queue=mQueue, event=mEvent, worker=mProcess) - - -# -------------------------------------------------------------------------- # -# Define our data transfer objects -# -------------------------------------------------------------------------- # -# These will be used to serialize state between the various workers. -# We use named tuples here as they are very lightweight while giving us -# all the benefits of classes. -# -------------------------------------------------------------------------- # -WorkRequest = namedtuple("WorkRequest", "request, work_id") -WorkResponse = namedtuple("WorkResponse", "is_exception, work_id, response") - - -# -------------------------------------------------------------------------- # -# Define our worker processes -# -------------------------------------------------------------------------- # -def _client_worker_process(factory, input_queue, output_queue, is_shutdown): - """Take input requests, - - issues them on its - client handle, and then sends the client response (success or failure) - to the manager to deliver back to the application. - - It should be noted that there are N of these workers and they can - be run in process or out of process as all the state serializes. - - :param factory: A client factory used to create a new client - :param input_queue: The queue to pull new requests to issue - :param output_queue: The queue to place client responses - :param is_shutdown: Condition variable marking process shutdown - """ - txt = f"starting up worker : {threading.current_thread()}" - log.info(txt) - my_client = factory() - while not is_shutdown.is_set(): - try: - workitem = input_queue.get(timeout=1) - txt = f"dequeue worker request: {workitem}" - log.debug(txt) - if not workitem: - continue - try: - txt = f"executing request on thread: {workitem}" - log.debug(txt) - result = my_client.execute(workitem.request) - output_queue.put(WorkResponse(False, workitem.work_id, result)) - except Exception as exc: # pylint: disable=broad-except - txt = f"error in worker thread: {threading.current_thread()}" - log.exception(txt) - output_queue.put(WorkResponse(True, workitem.work_id, exc)) - except Exception: # pylint: disable=broad-except - pass - txt = f"request worker shutting down: {threading.current_thread()}" - log.info(txt) - - -def _manager_worker_process(output_queue, my_futures, is_shutdown): - """Take output responses and tying them back to the future. - - keyed on the initial transaction id. - - Basically this can be thought of as the delivery worker. - - It should be noted that there are one of these threads and it must - be an in process thread as the futures will not serialize across - processes.. - - :param output_queue: The queue holding output results to return - :param futures: The mapping of tid -> future - :param is_shutdown: Condition variable marking process shutdown - """ - txt = f"starting up manager worker: {threading.current_thread()}" - log.info(txt) - while not is_shutdown.is_set(): - try: - workitem = output_queue.get() - my_future = my_futures.get(workitem.work_id, None) - txt = f"dequeue manager response: {workitem}" - log.debug(txt) - if not my_future: - continue - if workitem.is_exception: - my_future.set_exception(workitem.response) - else: - my_future.set_result(workitem.response) - txt = f"updated future result: {my_future}" - log.debug(txt) - del futures[workitem.work_id] - except Exception: # pylint: disable=broad-except - log.exception("error in manager") - txt = f"manager worker shutting down: {threading.current_thread()}" - log.info(txt) - - -# -------------------------------------------------------------------------- # -# Define our concurrent client -# -------------------------------------------------------------------------- # -class ConcurrentClient(ModbusClientMixin): - """This is a high performance client. - - that can be used to read/write a large number of requests at once asynchronously. - This operates with a backing worker pool of processes or threads - to achieve its performance. - """ - - def __init__(self, **kwargs): - """Initialize a new instance of the client.""" - worker_count = kwargs.get("count", multiprocessing.cpu_count()) - self.factory = kwargs.get("factory") - primitives = _Primitives.create(kwargs.get("in_process", False)) - self.is_shutdown = primitives.event() # process shutdown condition - self.input_queue = primitives.queue() # input requests to process - self.output_queue = primitives.queue() # output results to return - self.futures = {} # mapping of tid -> future - self.workers = [] # handle to our worker threads - self.counter = itertools.count() - - # creating the response manager - self.manager = threading.Thread( - target=_manager_worker_process, - args=(self.output_queue, self.futures, self.is_shutdown), - ) - self.manager.start() - self.workers.append(self.manager) - - # creating the request workers - for i in range(worker_count): - worker = primitives.worker( - target=_client_worker_process, - args=( - self.factory, - self.input_queue, - self.output_queue, - self.is_shutdown, - ), - ) - worker.start() - self.workers.append(worker) - - def shutdown(self): - """Shutdown all the workersbeing used to concurrently process the requests.""" - log.info("stating to shut down workers") - self.is_shutdown.set() - # to wake up the manager - self.output_queue.put(WorkResponse(None, None, None)) - for worker in self.workers: - worker.join() - log.info("finished shutting down workers") - - def execute(self, request): - """Given a request- - - enqueue it to be processed - and then return a future linked to the response - of the call. - - :param request: The request to execute - :returns: A future linked to the call's response - """ - fut, work_id = Future(), next(self.counter) - self.input_queue.put(WorkRequest(request, work_id)) - self.futures[work_id] = fut - return fut - - def execute_silently(self, request): - """Given a write request. - - enqueue it to be processed without worrying about calling the - application back (fire and forget) - - :param request: The request to execute - """ - self.input_queue.put(WorkRequest(request, None)) - - -if __name__ == "__main__": - from pymodbus.client import ModbusTcpClient - - def client_factory(): - """Client factory.""" - txt = f"creating client for: {threading.current_thread()}" - log.debug(txt) - my_client = ModbusTcpClient("127.0.0.1", port=5020) - my_client.connect() - return client - - client = ConcurrentClient(factory=client_factory) - try: - log.info("issuing concurrent requests") - futures = [client.read_coils(i * 8, 8) for i in range(10)] - log.info("waiting on futures to complete") - for future in futures: - txt = f"future result: {future.result(timeout=1)}" - log.info(txt) - finally: - client.shutdown() diff --git a/examples/v2.5.3/custom_datablock.py b/examples/v2.5.3/custom_datablock.py deleted file mode 100755 index a472f566a..000000000 --- a/examples/v2.5.3/custom_datablock.py +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env python3 -# pylint: disable=missing-type-doc,missing-param-doc,differing-param-doc -"""Pymodbus Server With Custom Datablock Side Effect. - -This is an example of performing custom logic after a value has been -written to the datastore. -""" -import logging - -# --------------------------------------------------------------------------- # -# import the modbus libraries we need -# --------------------------------------------------------------------------- # -from pymodbus import __version__ as pymodbus_version -from pymodbus.datastore import ( - ModbusServerContext, - ModbusSlaveContext, - ModbusSparseDataBlock, -) -from pymodbus.device import ModbusDeviceIdentification -from pymodbus.server import StartTcpServer - - -# from pymodbus.transaction import ModbusRtuFramer, ModbusAsciiFramer - -# --------------------------------------------------------------------------- # -# configure the service logging -# --------------------------------------------------------------------------- # -log = logging.getLogger() -log.setLevel(logging.DEBUG) - -# --------------------------------------------------------------------------- # -# create your custom data block here -# --------------------------------------------------------------------------- # - - -class CustomDataBlock(ModbusSparseDataBlock): - """A datablock that stores the new value in memory, - - and performs a custom action after it has been stored. - """ - - def setValues(self, address, value): # pylint: disable=arguments-differ - """Set the requested values of the datastore - - :param address: The starting address - :param values: The new values to be set - """ - super().setValues(address, value) - - # whatever you want to do with the written value is done here, - # however make sure not to do too much work here or it will - # block the server, espectially if the server is being written - # to very quickly - print(f"wrote {value} to {address}") - - -def run_custom_db_server(): - """Run custom db server.""" - # ----------------------------------------------------------------------- # - # initialize your data store - # ----------------------------------------------------------------------- # - block = CustomDataBlock([0] * 100) - store = ModbusSlaveContext(di=block, co=block, hr=block, ir=block) - context = ModbusServerContext(slaves=store, single=True) - - # ----------------------------------------------------------------------- # - # initialize the server information - # ----------------------------------------------------------------------- # - - identity = ModbusDeviceIdentification( - info_name={ - "VendorName": "pymodbus", - "ProductCode": "PM", - "VendorUrl": "https://github.com/pymodbus-dev/pymodbus/", - "ProductName": "pymodbus Server", - "ModelName": "pymodbus Server", - "MajorMinorRevision": pymodbus_version, - } - ) - - # ----------------------------------------------------------------------- # - # run the server you want - # ----------------------------------------------------------------------- # - - # p = Process(target=device_writer, args=(queue,)) - # p.start() - StartTcpServer(context, identity=identity, address=("localhost", 5020)) - - -if __name__ == "__main__": - run_custom_db_server() diff --git a/examples/v2.5.3/custom_synchronous_server.py b/examples/v2.5.3/custom_synchronous_server.py deleted file mode 100755 index 0f2bc62d8..000000000 --- a/examples/v2.5.3/custom_synchronous_server.py +++ /dev/null @@ -1,132 +0,0 @@ -#!/usr/bin/env python3 -"""Pymodbus Synchronous Server Example with Custom functions. - -Implements a custom function code not in standard modbus function code list -and its response which otherwise would throw `IllegalFunction (0x1)` error. - -Steps: -1. Create CustomModbusRequest class derived from ModbusRequest - ```class CustomModbusRequest(ModbusRequest): - function_code = 75 # Value less than 0x80) - _rtu_frame_size = # Required only For RTU support - - def __init__(custom_arg=None, **kwargs) - # Make sure the arguments has default values, will error out - # while decoding otherwise - ModbusRequest.__init__(self, **kwargs) - self.custom_request_arg = custom_arg - - def encode(self): - # Implement encoding logic - - def decode(self, data): - # implement decoding logic - - def execute(self, context): - # Implement execute logic - ... - return CustomModbusResponse(..) - - ``` -2. Create CustomModbusResponse class derived from ModbusResponse - ```class CustomModbusResponse(ModbusResponse): - function_code = 75 # Value less than 0x80) - _rtu_byte_count_pos = # Required only For RTU support - - def __init__(self, custom_args=None, **kwargs): - # Make sure the arguments has default values, will error out - # while decoding otherwise - ModbusResponse.__init__(self, **kwargs) - self.custom_reponse_values = values - - def encode(self): - # Implement encoding logic - def decode(self, data): - # Implement decoding logic - ``` -3. Register with ModbusSlaveContext, - if your request has to access some values from the data-store. - ```store = ModbusSlaveContext(...) - store.register(CustomModbusRequest.function_code, "dummy_context_name") - ``` -4. Pass CustomModbusRequest class as argument to StartServer - ``` - StartTcpServer(..., custom_functions=[CustomModbusRequest]..) - ``` - -""" -import logging - -from pymodbus import __version__ as pymodbus_version -from pymodbus.datastore import ( - ModbusSequentialDataBlock, - ModbusServerContext, - ModbusSlaveContext, -) -from pymodbus.device import ModbusDeviceIdentification -from pymodbus.server import StartTcpServer - -from .custom_message import ( # pylint: disable=relative-beyond-top-level - CustomModbusRequest, -) - - -# --------------------------------------------------------------------------- # -# configure the service logging -# --------------------------------------------------------------------------- # - -FORMAT = ( - "%(asctime)-15s %(threadName)-15s" - " %(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s" -) -logging.basicConfig(format=FORMAT) -log = logging.getLogger() -log.setLevel(logging.DEBUG) - - -def run_server(): - """Run server.""" - store = ModbusSlaveContext( - di=ModbusSequentialDataBlock(0, [17] * 100), - co=ModbusSequentialDataBlock(0, [17] * 100), - hr=ModbusSequentialDataBlock(0, [17] * 100), - ir=ModbusSequentialDataBlock(0, [17] * 100), - ) - - store.register( - CustomModbusRequest.function_code, - "cm", - ModbusSequentialDataBlock(0, [17] * 100), - ) - context = ModbusServerContext(slaves=store, single=True) - - # ----------------------------------------------------------------------- # - # initialize the server information - # ----------------------------------------------------------------------- # - # If you don't set this or any fields, they are defaulted to empty strings. - # ----------------------------------------------------------------------- # - identity = ModbusDeviceIdentification( - info_name={ - "VendorName": "Pymodbus", - "ProductCode": "PM", - "VendorUrl": "https://github.com/pymodbus-dev/pymodbus/", - "ProductName": "Pymodbus Server", - "ModelName": "Pymodbus Server", - "MajorMinorRevision": pymodbus_version, - } - ) - - # ----------------------------------------------------------------------- # - # run the server you want - # ----------------------------------------------------------------------- # - # Tcp: - StartTcpServer( - context, - identity=identity, - address=("localhost", 5020), - custom_functions=[CustomModbusRequest], - ) - - -if __name__ == "__main__": - run_server() diff --git a/examples/v2.5.3/dbstore_update_server.py b/examples/v2.5.3/dbstore_update_server.py deleted file mode 100644 index 3db3f0878..000000000 --- a/examples/v2.5.3/dbstore_update_server.py +++ /dev/null @@ -1,106 +0,0 @@ -# pylint: disable=differing-param-doc,missing-any-param-doc -"""Pymodbus Server With Updating Thread. - -This is an example of having a background thread updating the -context in an SQLite4 database while the server is operating. - -This scrit generates a random address range (within 0 - 65000) and a random -value and stores it in a database. It then reads the same address to verify -that the process works as expected - -This can also be done with a python thread:: - from threading import Thread - thread = Thread(target=updating_writer, args=(context,)) - thread.start() -""" -import asyncio -import logging -import random - -# --------------------------------------------------------------------------- # -# import the modbus libraries we need -# --------------------------------------------------------------------------- # -from pymodbus import __version__ as pymodbus_version -from pymodbus.datastore import ModbusSequentialDataBlock, ModbusServerContext -from pymodbus.datastore.database import SqlSlaveContext -from pymodbus.device import ModbusDeviceIdentification -from pymodbus.server import StartAsyncTcpServer - - -# from pymodbus.transaction import ModbusRtuFramer, ModbusAsciiFramer - -# --------------------------------------------------------------------------- # -# configure the service logging -# --------------------------------------------------------------------------- # -log = logging.getLogger() -log.setLevel(logging.DEBUG) - -# --------------------------------------------------------------------------- # -# define your callback process -# --------------------------------------------------------------------------- # - - -def updating_writer(parm1): - """Run every so often, - - and updates live values of the context which resides in an SQLite3 database. - It should be noted that there is a race condition for the update. - :param arguments: The input arguments to the call - """ - log.debug("Updating the database context") - context = parm1[0] - readfunction = 0x03 # read holding registers - writefunction = 0x10 - slave_id = 0x01 # slave address - count = 50 - - # import pdb; pdb.set_trace() - - rand_value = random.randint(0, 9999) - rand_addr = random.randint(0, 65000) - txt = f"Writing to datastore: {rand_addr}, {rand_value}" - log.debug(txt) - # import pdb; pdb.set_trace() - context[slave_id].setValues(writefunction, rand_addr, [rand_value], update=False) - values = context[slave_id].getValues(readfunction, rand_addr, count) - txt = f"Values from datastore: {values}" - log.debug(txt) - - -async def run_dbstore_update_server(): - """Run dbstore update server.""" - # ----------------------------------------------------------------------- # - # initialize your data store - # ----------------------------------------------------------------------- # - - block = ModbusSequentialDataBlock(0x00, [0] * 0xFF) - store = SqlSlaveContext(block) - - context = ModbusServerContext(slaves={1: store}, single=False) - - # ----------------------------------------------------------------------- # - # initialize the server information - # ----------------------------------------------------------------------- # - identity = ModbusDeviceIdentification( - info_name={ - "VendorName": "pymodbus", - "ProductCode": "PM", - "VendorUrl": "https://github.com/pymodbus-dev/pymodbus/", - "ProductName": "pymodbus Server", - "ModelName": "pymodbus Server", - "MajorMinorRevision": pymodbus_version, - } - ) - - # ----------------------------------------------------------------------- # - # run the server you want - # ----------------------------------------------------------------------- # - time = 5 # 5 seconds delay - loop = asyncio.get_event_loop() - loop.start(time, now=False) # initially delay by time - loop.stop() - await StartAsyncTcpServer(context, identity=identity, address=("", 5020)) - - -if __name__ == "__main__": - asyncio.run(run_dbstore_update_server()) diff --git a/examples/v2.5.3/device_mapping b/examples/v2.5.3/device_mapping deleted file mode 100644 index dc7c8b85c..000000000 --- a/examples/v2.5.3/device_mapping +++ /dev/null @@ -1,2 +0,0 @@ -0x0001,/dev/ptyp0 -0x0002,/dev/ptyp1 \ No newline at end of file diff --git a/examples/v2.5.3/deviceinfo_showcase_client.py b/examples/v2.5.3/deviceinfo_showcase_client.py deleted file mode 100755 index 6ac70caa1..000000000 --- a/examples/v2.5.3/deviceinfo_showcase_client.py +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env python3 -"""Pymodbus Synchronous Client Example to showcase Device Information. - -This client demonstrates the use of Device Information to get information -about servers connected to the client. This is part of the MODBUS specification, -and uses the MEI 0x2B 0x0E request / response. -""" -import logging - -# --------------------------------------------------------------------------- # -# import the various server implementations -# --------------------------------------------------------------------------- # -from pymodbus.client import ModbusTcpClient as ModbusClient -from pymodbus.device import ModbusDeviceIdentification - -# --------------------------------------------------------------------------- # -# import the request -# --------------------------------------------------------------------------- # -from pymodbus.mei_message import ReadDeviceInformationRequest - - -# from pymodbus.client import ModbusUdpClient as ModbusClient -# from pymodbus.client import ModbusSerialClient as ModbusClient - - -# --------------------------------------------------------------------------- # -# configure the client logging -# --------------------------------------------------------------------------- # -FORMAT = ( - "%(asctime)-15s %(threadName)-15s " - "%(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s" -) -logging.basicConfig(format=FORMAT) -log = logging.getLogger() -log.setLevel(logging.DEBUG) - -UNIT = 0x1 - - -def run_sync_client(): - """Run sync client.""" - # ------------------------------------------------------------------------# - # choose the client you want - # ------------------------------------------------------------------------# - # make sure to start an implementation to hit against. For this - # you can use an existing device, the reference implementation in the tools - # directory, or start a pymodbus server. - # - # If you use the UDP or TCP clients, you can override the framer being used - # to use a custom implementation (say RTU over TCP). By default they use - # the socket framer:: - # - # client = ModbusClient("localhost", port=5020, framer=ModbusRtuFramer) - # - # It should be noted that you can supply an ipv4 or an ipv6 host address - # for both the UDP and TCP clients. - # - # There are also other options that can be set on the client that controls - # how transactions are performed. The current ones are: - # - # * retries - Specify how many retries to allow per transaction (default=3) - # * retry_on_empty - Is an empty response a retry (default = False) - # * source_address - Specifies the TCP source address to bind to - # - # Here is an example of using these options:: - # - # client = ModbusClient("localhost", retries=3, retry_on_empty=True) - # ------------------------------------------------------------------------# - client = ModbusClient("localhost", port=5020) - # from pymodbus.transaction import ModbusRtuFramer - # client = ModbusClient("localhost", port=5020, framer=ModbusRtuFramer) - # client = ModbusClient(method="binary", port="/dev/ptyp0", timeout=1) - # client = ModbusClient(method="ascii", port="/dev/ptyp0", timeout=1) - # client = ModbusClient(method="rtu", port="/dev/ptyp0", timeout=1, - # baudrate=9600) - client.connect() - - # ------------------------------------------------------------------------# - # specify slave to query - # ------------------------------------------------------------------------# - # The slave to query is specified in an optional parameter for each - # individual request. This can be done by specifying the `unit` parameter - # which defaults to `0x00` - # ----------------------------------------------------------------------- # - log.debug("Reading Device Information") - information = {} - rr = None - - while not rr or rr.more_follows: - next_object_id = rr.next_object_id if rr else 0 - rq = ReadDeviceInformationRequest( - read_code=0x03, unit=UNIT, object_id=next_object_id - ) - rr = client.execute(rq) - information.update(rr.information) - log.debug(rr) - - print("Device Information : ") - for ( - key - ) in ( - information.keys() - ): # pylint: disable=consider-iterating-dictionary,consider-using-dict-items - print(key, information[key]) - - # ----------------------------------------------------------------------- # - # You can also have the information parsed through the - # ModbusDeviceIdentificiation class, which gets you a more usable way - # to access the Basic and Regular device information objects which are - # specifically listed in the Modbus specification - # ----------------------------------------------------------------------- # - device_id = ModbusDeviceIdentification(info=information) - print("Product Name : ", device_id.ProductName) - - # ----------------------------------------------------------------------- # - # close the client - # ----------------------------------------------------------------------- # - client.close() - - -if __name__ == "__main__": - run_sync_client() diff --git a/examples/v2.5.3/deviceinfo_showcase_server.py b/examples/v2.5.3/deviceinfo_showcase_server.py deleted file mode 100755 index 10f068e2a..000000000 --- a/examples/v2.5.3/deviceinfo_showcase_server.py +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env python3 -"""Pymodbus Synchronous Server Example to showcase Device Information. - -This server demonstrates the use of Device Information to provide information -to clients about the device. This is part of the MODBUS specification, and -uses the MEI 0x2B 0x0E request / response. This example creates an otherwise -empty server. -""" -import logging - -from serial import __version__ as pyserial_version - -from pymodbus import __version__ as pymodbus_version -from pymodbus.datastore import ModbusServerContext, ModbusSlaveContext - -# from pymodbus.server import StartUdpServer -# from pymodbus.server import StartSerialServer -# from pymodbus.transaction import ModbusRtuFramer, ModbusBinaryFramer -from pymodbus.device import ModbusDeviceIdentification -from pymodbus.server import StartTcpServer - - -# --------------------------------------------------------------------------- # -# configure the service logging -# --------------------------------------------------------------------------- # -FORMAT = ( - "%(asctime)-15s %(threadName)-15s" - " %(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s" -) -logging.basicConfig(format=FORMAT) -log = logging.getLogger() -log.setLevel(logging.DEBUG) - - -def run_server(): - """Run server.""" - # ----------------------------------------------------------------------- # - # initialize your data store - # ----------------------------------------------------------------------- # - store = ModbusSlaveContext() - context = ModbusServerContext(slaves=store, single=True) - - # ----------------------------------------------------------------------- # - # initialize the server information - # ----------------------------------------------------------------------- # - # If you don't set this or any fields, they are defaulted to empty strings. - # ----------------------------------------------------------------------- # - identity = ModbusDeviceIdentification( - info_name={ - "VendorName": "Pymodbus", - "ProductCode": "PM", - "VendorUrl": "https://github.com/pymodbus-dev/pymodbus/", - "ProductName": "Pymodbus Server", - "ModelName": "Pymodbus Server", - "MajorMinorRevision": pymodbus_version, - } - ) - - # ----------------------------------------------------------------------- # - # Add an example which is long enough to force the ReadDeviceInformation - # request / response to require multiple responses to send back all of the - # information. - # ----------------------------------------------------------------------- # - - identity[0x80] = ( - "Lorem ipsum dolor sit amet, consectetur adipiscing " - "elit. Vivamus rhoncus massa turpis, sit amet " - "ultrices orci semper ut. Aliquam tristique sapien in " - "lacus pharetra, in convallis nunc consectetur. Nunc " - "velit elit, vehicula tempus tempus sed. " - ) - - # ----------------------------------------------------------------------- # - # Add an example with repeated object IDs. The MODBUS specification is - # entirely silent on whether or not this is allowed. In practice, this - # should be assumed to be contrary to the MODBUS specification and other - # clients (other than pymodbus) might behave differently when presented - # with an object ID occurring twice in the returned information. - # - # Use this at your discretion, and at the very least ensure that all - # objects which share a single object ID can fit together within a single - # ADU unit. In the case of Modbus RTU, this is about 240 bytes or so. In - # other words, when the spec says "An object is indivisible, therefore - # any object must have a size consistent with the size of transaction - # response", if you use repeated OIDs, apply that rule to the entire - # grouping of objects with the repeated OID. - # ----------------------------------------------------------------------- # - identity[0x81] = [f"pymodbus {pymodbus_version}", f"pyserial {pyserial_version}"] - - # ----------------------------------------------------------------------- # - # run the server you want - # ----------------------------------------------------------------------- # - # Tcp: - StartTcpServer(context, identity=identity, address=("localhost", 5020)) - - # TCP with different framer - # StartTcpServer(context, identity=identity, - # framer=ModbusRtuFramer, address=("0.0.0.0", 5020)) - - # Udp: - # StartUdpServer(context, identity=identity, address=("0.0.0.0", 5020)) - - # Ascii: - # StartSerialServer(context, identity=identity, - # port="/dev/ttyp0", timeout=1) - - # RTU: - # StartSerialServer(context, framer=ModbusRtuFramer, identity=identity, - # port="/dev/ttyp0", timeout=.005, baudrate=9600) - - # Binary - # StartSerialServer(context, - # identity=identity, - # framer=ModbusBinaryFramer, - # port="/dev/ttyp0", - # timeout=1) - - -if __name__ == "__main__": - run_server() diff --git a/examples/v2.5.3/libmodbus_client.py b/examples/v2.5.3/libmodbus_client.py deleted file mode 100755 index b6af5564d..000000000 --- a/examples/v2.5.3/libmodbus_client.py +++ /dev/null @@ -1,499 +0,0 @@ -#!/usr/bin/env python3 -# pylint: disable=missing-type-doc,missing-param-doc,differing-param-doc,missing-raises-doc -"""Libmodbus Protocol Wrapper. - -What follows is an example wrapper of the libmodbus library -(https://libmodbus.org/documentation/) for use with pymodbus. -There are two utilities involved here: - -* LibmodbusLevel1Client - - This is simply a python wrapper around the c library. It is - mostly a clone of the pylibmodbus implementation, but I plan - on extending it to implement all the available protocol using - the raw execute methods. - -* LibmodbusClient - - This is just another modbus client that can be used just like - any other client in pymodbus. - -For these to work, you must have `cffi` and `libmodbus-dev` installed: - - sudo apt-get install libmodbus-dev - pip install cffi -""" -# -------------------------------------------------------------------------- # -# import system libraries -# -------------------------------------------------------------------------- # -from cffi import FFI # pylint: disable=import-error - -from pymodbus.bit_read_message import ( - ReadCoilsResponse, - ReadDiscreteInputsResponse, -) -from pymodbus.bit_write_message import ( - WriteMultipleCoilsResponse, - WriteSingleCoilResponse, -) -from pymodbus.client.mixin import ModbusClientMixin -from pymodbus.constants import Defaults -from pymodbus.exceptions import ModbusException -from pymodbus.register_read_message import ( - ReadHoldingRegistersResponse, - ReadInputRegistersResponse, - ReadWriteMultipleRegistersResponse, -) -from pymodbus.register_write_message import ( - WriteMultipleRegistersResponse, - WriteSingleRegisterResponse, -) - - -# -------------------------------------------------------------------------- # -# import pymodbus libraries -# -------------------------------------------------------------------------- # - - -# --------------------------------------------------------------------------- # -# create the C interface -# --------------------------------------------------------------------------- # -# * TODO add the protocol needed for the servers -# --------------------------------------------------------------------------- # - -compiler = FFI() -compiler.cdef( - """ - typedef struct _modbus modbus_t; - - int modbus_connect(modbus_t *ctx); - int modbus_flush(modbus_t *ctx); - void modbus_close(modbus_t *ctx); - - const char *modbus_strerror(int errnum); - int modbus_set_slave(modbus_t *ctx, int slave); - - void modbus_get_response_timeout(modbus_t *ctx, uint32_t *to_sec, uint32_t *to_usec); - void modbus_set_response_timeout(modbus_t *ctx, uint32_t to_sec, uint32_t to_usec); - - int modbus_read_bits(modbus_t *ctx, int addr, int nb, uint8_t *dest); - int modbus_read_input_bits(modbus_t *ctx, int addr, int nb, uint8_t *dest); - int modbus_read_registers(modbus_t *ctx, int addr, int nb, uint16_t *dest); - int modbus_read_input_registers(modbus_t *ctx, int addr, int nb, uint16_t *dest); - - int modbus_write_bit(modbus_t *ctx, int coil_addr, int status); - int modbus_write_bits(modbus_t *ctx, int addr, int nb, const uint8_t *data); - int modbus_write_register(modbus_t *ctx, int reg_addr, int value); - int modbus_write_registers(modbus_t *ctx, int addr, int nb, const uint16_t *data); - int modbus_write_and_read_registers(modbus_t *ctx, int write_addr, int write_nb, - const uint16_t *src, int read_addr, int read_nb, uint16_t *dest); - - int modbus_mask_write_register(modbus_t *ctx, int addr, uint16_t and_mask, uint16_t or_mask); - int modbus_send_raw_request(modbus_t *ctx, uint8_t *raw_req, int raw_req_length); - - float modbus_get_float(const uint16_t *src); - void modbus_set_float(float f, uint16_t *dest); - - modbus_t* modbus_new_tcp(const char *ip_address, int port); - modbus_t* modbus_new_rtu(const char *device, int baud, char parity, int data_bit, int stop_bit); - void modbus_free(modbus_t *ctx); - - int modbus_receive(modbus_t *ctx, uint8_t *req); - int modbus_receive_from(modbus_t *ctx, int sockfd, uint8_t *req); - int modbus_receive_confirmation(modbus_t *ctx, uint8_t *rsp); -""" -) -LIB = compiler.dlopen("modbus") # create our bindings - -# -------------------------------------------------------------------------- # -# helper utilities -# -------------------------------------------------------------------------- # - - -def get_float(data): - """Get float.""" - return LIB.modbus_get_float(data) - - -def set_float(value, data): - """Set float.""" - LIB.modbus_set_float(value, data) - - -def cast_to_int16(data): - """Cast to int16.""" - return int(compiler.cast("int16_t", data)) - - -def cast_to_int32(data): - """Cast to int32.""" - return int(compiler.cast("int32_t", data)) - - -class NotImplementedException(Exception): - """Not implemented exception.""" - - -# -------------------------------------------------------------------------- # -# level1 client -# -------------------------------------------------------------------------- # - - -class LibmodbusLevel1Client: - """A raw wrapper around the libmodbus c library. - - Feel free to use it if you want increased performance and don't mind the - entire protocol not being implemented. - """ - - @classmethod - def create_tcp_client(cls, my_host="127.0.0.1", my_port=Defaults.TcpPort): - """Create a TCP modbus client for the supplied parameters. - - :param host: The host to connect to - :param port: The port to connect to on that host - :returns: A new level1 client - """ - my_client = LIB.modbus_new_tcp(my_host.encode(), my_port) - return cls(my_client) - - @classmethod - def create_rtu_client(cls, **kwargs): - """Create a TCP modbus client for the supplied parameters. - - :param port: The serial port to attach to - :param stopbits: The number of stop bits to use - :param bytesize: The bytesize of the serial messages - :param parity: Which kind of parity to use - :param baudrate: The baud rate to use for the serial device - :returns: A new level1 client - """ - my_port = kwargs.get("port", "/dev/ttyS0") - baudrate = kwargs.get("baud", Defaults.Baudrate) - parity = kwargs.get("parity", Defaults.Parity) - bytesize = kwargs.get("bytesize", Defaults.Bytesize) - stopbits = kwargs.get("stopbits", Defaults.Stopbits) - my_client = LIB.modbus_new_rtu(my_port, baudrate, parity, bytesize, stopbits) - return cls(my_client) - - def __init__(self, my_client): - """Initialize a new instance of the LibmodbusLevel1Client. - - This method should not be used, instead new instances should be created - using the two supplied factory methods: - - * LibmodbusLevel1Client.create_rtu_client(...) - * LibmodbusLevel1Client.create_tcp_client(...) - - :param client: The underlying client instance to operate with. - """ - self.client = my_client - self.slave = Defaults.Slave - - def set_slave(self, slave): - """Set the current slave to operate against. - - :param slave: The new slave to operate against - :returns: The resulting slave to operate against - """ - self.slave = self._execute( # pylint: disable=no-member - LIB.modbus_set_slave, slave - ) - return self.slave - - def connect(self): - """Attempt to connect to the client target. - - :returns: True if successful, throws otherwise - """ - return not self.__execute(LIB.modbus_connect) - - def flush(self): - """Discard the existing bytes on the wire. - - :returns: The number of flushed bytes, or throws - """ - return self.__execute(LIB.modbus_flush) - - def close(self): - """Close and frees the underlying connection and context structure. - - :returns: Always True - """ - LIB.modbus_close(self.client) - LIB.modbus_free(self.client) - return True - - def __execute(self, command, *args): - """Run the supplied command against the currently instantiated client with the supplied arguments. - - This will make sure to correctly handle resulting errors. - - :param command: The command to execute against the context - :param *args: The arguments for the given command - :returns: The result of the operation unless -1 which throws - """ - if (result := command(self.client, *args)) == -1: - message = LIB.modbus_strerror(compiler.errno) - raise ModbusException(compiler.string(message)) - return result - - def read_bits(self, address, count=1): - """Read bits. - - :param address: The starting address to read from - :param count: The number of coils to read - :returns: The resulting bits - """ - result = compiler.new("uint8_t[]", count) - self.__execute(LIB.modbus_read_bits, address, count, result) - return result - - def read_input_bits(self, address, count=1): - """Read input bits. - - :param address: The starting address to read from - :param count: The number of discretes to read - :returns: The resulting bits - """ - result = compiler.new("uint8_t[]", count) - self.__execute(LIB.modbus_read_input_bits, address, count, result) - return result - - def write_bit(self, address, value): - """Write bit. - - :param address: The starting address to write to - :param value: The value to write to the specified address - :returns: The number of written bits - """ - return self.__execute(LIB.modbus_write_bit, address, value) - - def write_bits(self, address, values): - """Write bits. - - :param address: The starting address to write to - :param values: The values to write to the specified address - :returns: The number of written bits - """ - count = len(values) - return self.__execute(LIB.modbus_write_bits, address, count, values) - - def write_register(self, address, value): - """Write register. - - :param address: The starting address to write to - :param value: The value to write to the specified address - :returns: The number of written registers - """ - return self.__execute(LIB.modbus_write_register, address, value) - - def write_registers(self, address, values): - """Write registers. - - :param address: The starting address to write to - :param values: The values to write to the specified address - :returns: The number of written registers - """ - count = len(values) - return self.__execute(LIB.modbus_write_registers, address, count, values) - - def read_registers(self, address, count=1): - """Read registers. - - :param address: The starting address to read from - :param count: The number of registers to read - :returns: The resulting read registers - """ - result = compiler.new("uint16_t[]", count) - self.__execute(LIB.modbus_read_registers, address, count, result) - return result - - def read_input_registers(self, address, count=1): - """Read input registers. - - :param address: The starting address to read from - :param count: The number of registers to read - :returns: The resulting read registers - """ - result = compiler.new("uint16_t[]", count) - self.__execute(LIB.modbus_read_input_registers, address, count, result) - return result - - def read_and_write_registers( - self, read_address, read_count, write_address, write_registers - ): - """Read/write registers. - - :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 write_registers: The registers to write to the specified address - :returns: The resulting read registers - """ - write_count = len(write_registers) - read_result = compiler.new("uint16_t[]", read_count) - self.__execute( - LIB.modbus_write_and_read_registers, - write_address, - write_count, - write_registers, - read_address, - read_count, - read_result, - ) - return read_result - - -# -------------------------------------------------------------------------- # -# level2 client -# -------------------------------------------------------------------------- # - - -class LibmodbusClient(ModbusClientMixin): - """A facade around the raw level 1 libmodbus client. - - that implements the pymodbus protocol on top of the lower level client. - """ - - # ----------------------------------------------------------------------- # - # these are used to convert from the pymodbus request types to the - # libmodbus operations (overloaded operator). - # ----------------------------------------------------------------------- # - - __methods = { - "ReadCoilsRequest": lambda c, r: c.read_bits(r.address, r.count), - "ReadDiscreteInputsRequest": lambda c, r: c.read_input_bits(r.address, r.count), - "WriteSingleCoilRequest": lambda c, r: c.write_bit(r.address, r.value), - "WriteMultipleCoilsRequest": lambda c, r: c.write_bits(r.address, r.values), - "WriteSingleRegisterRequest": lambda c, r: c.write_register(r.address, r.value), - "WriteMultipleRegistersRequest": lambda c, r: c.write_registers( - r.address, r.values - ), - "ReadHoldingRegistersRequest": lambda c, r: c.read_registers( - r.address, r.count - ), - "ReadInputRegistersRequest": lambda c, r: c.read_input_registers( - r.address, r.count - ), - "ReadWriteMultipleRegistersRequest": lambda c, r: c.read_and_write_registers( - r.read_address, r.read_count, r.write_address, r.write_registers - ), - } - - # ----------------------------------------------------------------------- # - # these are used to convert from the libmodbus result to the - # pymodbus response type - # ----------------------------------------------------------------------- # - - __adapters = { - "ReadCoilsRequest": lambda tx, rx: ReadCoilsResponse(list(rx)), - "ReadDiscreteInputsRequest": lambda tx, rx: ReadDiscreteInputsResponse( - list(rx) - ), - "WriteSingleCoilRequest": lambda tx, rx: WriteSingleCoilResponse( - tx.address, rx - ), - "WriteMultipleCoilsRequest": lambda tx, rx: WriteMultipleCoilsResponse( - tx.address, rx - ), - "WriteSingleRegisterRequest": lambda tx, rx: WriteSingleRegisterResponse( - tx.address, rx - ), - "WriteMultipleRegistersRequest": lambda tx, rx: WriteMultipleRegistersResponse( - tx.address, rx - ), - "ReadHoldingRegistersRequest": lambda tx, rx: ReadHoldingRegistersResponse( - list(rx) - ), - "ReadInputRegistersRequest": lambda tx, rx: ReadInputRegistersResponse( - list(rx) - ), - "ReadWriteMultipleRegistersRequest": lambda tx, rx: ReadWriteMultipleRegistersResponse( - list(rx) - ), - } - - def __init__(self, my_client): - """Initialize a new instance of the LibmodbusClient. - - This should be initialized with one of the LibmodbusLevel1Client instances: - - * LibmodbusLevel1Client.create_rtu_client(...) - * LibmodbusLevel1Client.create_tcp_client(...) - :param client: The underlying client instance to operate with. - """ - self.client = my_client - - # ----------------------------------------------------------------------- # - # We use the client mixin to implement the api methods which are all - # forwarded to this method. It is implemented using the previously - # defined lookup tables. Any method not defined simply throws. - # ----------------------------------------------------------------------- # - - def execute(self, request): - """Execute the supplied request against the server. - - :param request: The request to process - :returns: The result of the request execution - """ - if self.client.slave != request.unit_id: - self.client.set_slave(request.unit_id) - - method = request.__class__.__name__ - operation = self.__methods.get(method, None) - adapter = self.__adapters.get(method, None) - - if not operation or not adapter: - raise NotImplementedException("Method not implemented: " + operation) - - response = operation(self.client, request) - return adapter(request, response) - - # ----------------------------------------------------------------------- # - # Other methods can simply be forwarded using the decorator pattern - # ----------------------------------------------------------------------- # - - def connect(self): - """Connect.""" - return self.client.connect() - - def close(self): - """Close.""" - return self.client.close() - - # ----------------------------------------------------------------------- # - # magic methods - # ----------------------------------------------------------------------- # - - def __enter__(self): - """Implement the client with enter block - - :returns: The current instance of the client - """ - self.client.connect() - return self - - def __exit__(self, klass, value, traceback): - """Implement the client with exit block""" - self.client.close() - - -# -------------------------------------------------------------------------- # -# main example runner -# -------------------------------------------------------------------------- # - - -if __name__ == "__main__": - - # create our low level client - host = "127.0.0.1" # pylint: disable=invalid-name - port = 502 # pylint: disable=invalid-name - protocol = LibmodbusLevel1Client.create_tcp_client(host, port) - - # operate with our high level client - with LibmodbusClient(protocol) as client: - registers = client.write_registers(0, [13, 12, 11]) - print(registers) - registers = client.read_holding_registers(0, 10) - print(registers.registers) diff --git a/examples/v2.5.3/message_generator.py b/examples/v2.5.3/message_generator.py deleted file mode 100755 index c27c425a0..000000000 --- a/examples/v2.5.3/message_generator.py +++ /dev/null @@ -1,295 +0,0 @@ -#!/usr/bin/env python3 -# pylint: disable=missing-type-doc -"""Modbus Message Generator. - -The following is an example of how to generate example encoded messages -for the supplied modbus format: - -* tcp - `./generate-messages.py -f tcp -m rx -b` -* ascii - `./generate-messages.py -f ascii -m tx -a` -* rtu - `./generate-messages.py -f rtu -m rx -b` -* binary - `./generate-messages.py -f binary -m tx -b` -""" -import codecs as c -import logging -from optparse import OptionParser # pylint: disable=deprecated-module - -import pymodbus.diag_message as modbus_diag -from pymodbus.bit_read_message import ( - ReadCoilsRequest, - ReadCoilsResponse, - ReadDiscreteInputsRequest, - ReadDiscreteInputsResponse, -) -from pymodbus.bit_write_message import ( - WriteMultipleCoilsRequest, - WriteMultipleCoilsResponse, - WriteSingleCoilRequest, - WriteSingleCoilResponse, -) -from pymodbus.file_message import ( - ReadFifoQueueRequest, - ReadFifoQueueResponse, - ReadFileRecordRequest, - ReadFileRecordResponse, - WriteFileRecordRequest, - WriteFileRecordResponse, -) -from pymodbus.mei_message import ( - ReadDeviceInformationRequest, - ReadDeviceInformationResponse, -) -from pymodbus.other_message import ( - GetCommEventCounterRequest, - GetCommEventCounterResponse, - GetCommEventLogRequest, - GetCommEventLogResponse, - ReadExceptionStatusRequest, - ReadExceptionStatusResponse, - ReportSlaveIdRequest, - ReportSlaveIdResponse, -) -from pymodbus.register_read_message import ( - ReadHoldingRegistersRequest, - ReadHoldingRegistersResponse, - ReadInputRegistersRequest, - ReadInputRegistersResponse, - ReadWriteMultipleRegistersRequest, - ReadWriteMultipleRegistersResponse, -) -from pymodbus.register_write_message import ( - MaskWriteRegisterRequest, - MaskWriteRegisterResponse, - WriteMultipleRegistersRequest, - WriteMultipleRegistersResponse, - WriteSingleRegisterRequest, - WriteSingleRegisterResponse, -) - -# -------------------------------------------------------------------------- # -# import all the available framers -# -------------------------------------------------------------------------- # -from pymodbus.transaction import ( - ModbusAsciiFramer, - ModbusBinaryFramer, - ModbusRtuFramer, - ModbusSocketFramer, -) - - -# -------------------------------------------------------------------------- # -# initialize logging -# -------------------------------------------------------------------------- # -modbus_log = logging.getLogger("pymodbus") - - -# -------------------------------------------------------------------------- # -# enumerate all request messages -# -------------------------------------------------------------------------- # -_request_messages = [ - ReadHoldingRegistersRequest, - ReadDiscreteInputsRequest, - ReadInputRegistersRequest, - ReadCoilsRequest, - WriteMultipleCoilsRequest, - WriteMultipleRegistersRequest, - WriteSingleRegisterRequest, - WriteSingleCoilRequest, - ReadWriteMultipleRegistersRequest, - ReadExceptionStatusRequest, - GetCommEventCounterRequest, - GetCommEventLogRequest, - ReportSlaveIdRequest, - ReadFileRecordRequest, - WriteFileRecordRequest, - MaskWriteRegisterRequest, - ReadFifoQueueRequest, - ReadDeviceInformationRequest, - modbus_diag.ReturnQueryDataRequest, - modbus_diag.RestartCommunicationsOptionRequest, - modbus_diag.ReturnDiagnosticRegisterRequest, - modbus_diag.ChangeAsciiInputDelimiterRequest, - modbus_diag.ForceListenOnlyModeRequest, - modbus_diag.ClearCountersRequest, - modbus_diag.ReturnBusMessageCountRequest, - modbus_diag.ReturnBusCommunicationErrorCountRequest, - modbus_diag.ReturnBusExceptionErrorCountRequest, - modbus_diag.ReturnSlaveMessageCountRequest, - modbus_diag.ReturnSlaveNoResponseCountRequest, - modbus_diag.ReturnSlaveNAKCountRequest, - modbus_diag.ReturnSlaveBusyCountRequest, - modbus_diag.ReturnSlaveBusCharacterOverrunCountRequest, - modbus_diag.ReturnIopOverrunCountRequest, - modbus_diag.ClearOverrunCountRequest, - modbus_diag.GetClearModbusPlusRequest, -] - - -# -------------------------------------------------------------------------- # -# enumerate all response messages -# -------------------------------------------------------------------------- # -_response_messages = [ - ReadHoldingRegistersResponse, - ReadDiscreteInputsResponse, - ReadInputRegistersResponse, - ReadCoilsResponse, - WriteMultipleCoilsResponse, - WriteMultipleRegistersResponse, - WriteSingleRegisterResponse, - WriteSingleCoilResponse, - ReadWriteMultipleRegistersResponse, - ReadExceptionStatusResponse, - GetCommEventCounterResponse, - GetCommEventLogResponse, - ReportSlaveIdResponse, - ReadFileRecordResponse, - WriteFileRecordResponse, - MaskWriteRegisterResponse, - ReadFifoQueueResponse, - ReadDeviceInformationResponse, - modbus_diag.ReturnQueryDataResponse, - modbus_diag.RestartCommunicationsOptionResponse, - modbus_diag.ReturnDiagnosticRegisterResponse, - modbus_diag.ChangeAsciiInputDelimiterResponse, - modbus_diag.ForceListenOnlyModeResponse, - modbus_diag.ClearCountersResponse, - modbus_diag.ReturnBusMessageCountResponse, - modbus_diag.ReturnBusCommunicationErrorCountResponse, - modbus_diag.ReturnBusExceptionErrorCountResponse, - modbus_diag.ReturnSlaveMessageCountResponse, - modbus_diag.ReturnSlaveNoReponseCountResponse, - modbus_diag.ReturnSlaveNAKCountResponse, - modbus_diag.ReturnSlaveBusyCountResponse, - modbus_diag.ReturnSlaveBusCharacterOverrunCountResponse, - modbus_diag.ReturnIopOverrunCountResponse, - modbus_diag.ClearOverrunCountResponse, - modbus_diag.GetClearModbusPlusResponse, -] - - -# -------------------------------------------------------------------------- # -# build an arguments singleton -# -------------------------------------------------------------------------- # -# Feel free to override any values here to generate a specific message -# in question. It should be noted that many argument names are reused -# between different messages, and a number of messages are simply using -# their default values. -# -------------------------------------------------------------------------- # -_arguments = { - "address": 0x12, - "count": 0x08, - "value": 0x01, - "values": [0x01] * 8, - "read_address": 0x12, - "read_count": 0x08, - "write_address": 0x12, - "write_registers": [0x01] * 8, - "transaction": 0x01, - "protocol": 0x00, - "unit": 0xFF, -} - - -# -------------------------------------------------------------------------- # -# generate all the requested messages -# -------------------------------------------------------------------------- # -def generate_messages(framer, options): - """Parse the command line options - - :param framer: The framer to encode the messages with - :param options: The message options to use - """ - if options.messages == "tx": - messages = _request_messages - else: - messages = _response_messages - for message in messages: - message = message(**_arguments) - print( - "%-44s = " # pylint: disable=consider-using-f-string - % message.__class__.__name__ - ) - packet = framer.buildPacket(message) - if not options.ascii: - packet = c.encode(packet, "hex_codec").decode("utf-8") - print(f"{packet}\n") # because ascii ends with a \r\n - - -# -------------------------------------------------------------------------- # -# initialize our program settings -# -------------------------------------------------------------------------- # -def get_options(): - """Parse the command line options - - :returns: The options manager - """ - parser = OptionParser() - - parser.add_option( - "-f", - "--framer", - help="The type of framer to use (tcp, rtu, binary, ascii)", - dest="framer", - default="tcp", - ) - - parser.add_option( - "-D", - "--debug", - help="Enable debug tracing", - action="store_true", - dest="debug", - default=False, - ) - - parser.add_option( - "-a", - "--ascii", - help="The indicates that the message is ascii", - action="store_true", - dest="ascii", - default=True, - ) - - parser.add_option( - "-b", - "--binary", - help="The indicates that the message is binary", - action="store_false", - dest="ascii", - ) - - parser.add_option( - "-m", - "--messages", - help="The messages to encode (rx, tx)", - dest="messages", - default="rx", - ) - - (opt, _) = parser.parse_args() - return opt - - -def main(): - """Run main runner function""" - option = get_options() - - if option.debug: - try: - modbus_log.setLevel(logging.DEBUG) - except Exception: # pylint: disable=broad-except - print("Logging is not supported on this system") - - framer = { - "tcp": ModbusSocketFramer, - "rtu": ModbusRtuFramer, - "binary": ModbusBinaryFramer, - "ascii": ModbusAsciiFramer, - }.get(option.framer, ModbusSocketFramer)(None) - - generate_messages(framer, option) - - -if __name__ == "__main__": - main() diff --git a/examples/v2.5.3/message_parser.py b/examples/v2.5.3/message_parser.py deleted file mode 100755 index a076ec3d5..000000000 --- a/examples/v2.5.3/message_parser.py +++ /dev/null @@ -1,270 +0,0 @@ -#!/usr/bin/env python3 -# pylint: disable=missing-type-doc,missing-param-doc,differing-param-doc,missing-any-param-doc -"""Modbus Message Parser. - -The following is an example of how to parse modbus messages -using the supplied framers for a number of protocols: - -* tcp -* ascii -* rtu -* binary -""" -# -------------------------------------------------------------------------- # -# import needed libraries -# -------------------------------------------------------------------------- # - -import codecs as c -import collections -import logging -import textwrap -from optparse import OptionParser # pylint: disable=deprecated-module - -from pymodbus.factory import ClientDecoder, ServerDecoder -from pymodbus.transaction import ( - ModbusAsciiFramer, - ModbusBinaryFramer, - ModbusRtuFramer, - ModbusSocketFramer, -) - - -# -------------------------------------------------------------------------- # -# -------------------------------------------------------------------------- # -FORMAT = ( - "%(asctime)-15s %(threadName)-15s" - " %(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s" -) -logging.basicConfig(format=FORMAT) -log = logging.getLogger() - - -# -------------------------------------------------------------------------- # -# build a quick wrapper around the framers -# -------------------------------------------------------------------------- # - - -class Decoder: - """Decoder.""" - - def __init__(self, framer, encode=False): - """Initialize a new instance of the decoder - - :param framer: The framer to use - :param encode: If the message needs to be encoded - """ - self.framer = framer - self.encode = encode - - def decode(self, message): - """Attempt to decode the supplied message - - :param message: The message to decode - """ - value = message if self.encode else c.encode(message, "hex_codec") - print("=" * 80) - print(f"Decoding Message {value}") - print("=" * 80) - decoders = [ - self.framer(ServerDecoder(), client=None), - self.framer(ClientDecoder(), client=None), - ] - for decoder in decoders: - print(f"{decoder.decoder.__class__.__name__}") - print("-" * 80) - try: - decoder.addToFrame(message) - if decoder.checkFrame(): - unit = decoder._header.get( # pylint: disable=protected-access - "uid", 0x00 - ) - decoder.advanceFrame() - decoder.processIncomingPacket(message, self.report, unit) - else: - self.check_errors(decoder, message) - except Exception: # pylint: disable=broad-except - self.check_errors(decoder, message) - - def check_errors(self, decoder, message): - """Attempt to find message errors - - :param message: The message to find errors in - """ - txt = f"Unable to parse message - {message} with {decoder}" - log.error(txt) - - def report(self, message): - """Print the message information - - :param message: The message to print - """ - print( - "%-15s = %s" # pylint: disable=consider-using-f-string - % ( - "name", - message.__class__.__name__, - ) - ) - for (k_dict, v_dict) in message.__dict__.items(): - if isinstance(v_dict, dict): - print("%-15s =" % k_dict) # pylint: disable=consider-using-f-string - for k_item, v_item in v_dict.items(): - print( - " %-12s => %s" # pylint: disable=consider-using-f-string - % (k_item, v_item) - ) - - elif isinstance(v_dict, collections.abc.Iterable): - print("%-15s =" % k_dict) # pylint: disable=consider-using-f-string - value = str([int(x) for x in v_dict]) - for line in textwrap.wrap(value, 60): - print( - "%-15s . %s" # pylint: disable=consider-using-f-string - % ("", line) - ) - else: - print( - "%-15s = %s" # pylint: disable=consider-using-f-string - % (k_dict, hex(v_dict)) - ) - print( - "%-15s = %s" # pylint: disable=consider-using-f-string - % ( - "documentation", - message.__doc__, - ) - ) - - -# -------------------------------------------------------------------------- # -# and decode our message -# -------------------------------------------------------------------------- # -def get_options(): - """Parse the command line options - - :returns: The options manager - """ - parser = OptionParser() - - parser.add_option( - "-p", - "--parser", - help="The type of parser to use (tcp, rtu, binary, ascii)", - dest="parser", - default="tcp", - ) - - parser.add_option( - "-D", - "--debug", - help="Enable debug tracing", - action="store_true", - dest="debug", - default=False, - ) - - parser.add_option( - "-m", "--message", help="The message to parse", dest="message", default=None - ) - - parser.add_option( - "-a", - "--ascii", - help="The indicates that the message is ascii", - action="store_true", - dest="ascii", - default=False, - ) - - parser.add_option( - "-b", - "--binary", - help="The indicates that the message is binary", - action="store_false", - dest="ascii", - ) - - parser.add_option( - "-f", - "--file", - help="The file containing messages to parse", - dest="file", - default=None, - ) - - parser.add_option( - "-t", - "--transaction", - help="If the incoming message is in hexadecimal format", - action="store_true", - dest="transaction", - default=False, - ) - parser.add_option( - "--framer", - help="Framer to use", - dest="framer", - default=None, - ) - - (opt, arg) = parser.parse_args() - - if not opt.message and len(arg) > 0: - opt.message = arg[0] - - return opt - - -def get_messages(option): - """Do a helper method to generate the messages to parse - - :param options: The option manager - :returns: The message iterator to parse - """ - if option.message: - if option.transaction: - msg = "" - for segment in option.message.split(): - segment = segment.replace("0x", "") - segment = "0" + segment if len(segment) == 1 else segment - msg = msg + segment - option.message = msg - - if not option.ascii: - option.message = c.decode(option.message.encode(), "hex_codec") - yield option.message - elif option.file: - with open(option.file, "r") as handle: # pylint: disable=unspecified-encoding - for line in handle: - if line.startswith("#"): - continue - if not option.ascii: - line = line.strip() - line = line.decode("hex") - yield line - - -def main(): - """Run main runner function""" - option = get_options() - - if option.debug: - try: - log.setLevel(logging.DEBUG) - except Exception as exc: # pylint: disable=broad-except - print(f"Logging is not supported on this system- {exc}") - - framer = { - "tcp": ModbusSocketFramer, - "rtu": ModbusRtuFramer, - "binary": ModbusBinaryFramer, - "ascii": ModbusAsciiFramer, - }.get(option.framer or option.parser, ModbusSocketFramer) - - decoder = Decoder(framer, option.ascii) - for message in get_messages(option): - decoder.decode(message) - - -if __name__ == "__main__": - main() diff --git a/examples/v2.5.3/modbus_logging.py b/examples/v2.5.3/modbus_logging.py deleted file mode 100755 index 3d8576291..000000000 --- a/examples/v2.5.3/modbus_logging.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python3 -"""Pymodbus Logging Examples.""" -import logging -import logging.handlers as Handlers - - -if __name__ == "__main__": - # ----------------------------------------------------------------------- # - # This will simply send everything logged to console - # ----------------------------------------------------------------------- # - log = logging.getLogger() - log.setLevel(logging.DEBUG) - - # ----------------------------------------------------------------------- # - # This will send the error messages in the specified namespace to a file. - # The available namespaces in pymodbus are as follows: - # ----------------------------------------------------------------------- # - # * pymodbus.* - The root namespace - # * pymodbus.server.* - all logging messages involving the modbus server - # * pymodbus.client.* - all logging messages involving the client - # * pymodbus.protocol.* - all logging messages inside the protocol layer - # ----------------------------------------------------------------------- # - log = logging.getLogger("pymodbus.server") - log.setLevel(logging.ERROR) - - # ----------------------------------------------------------------------- # - # This will send the error messages to the specified handlers: - # * docs.python.org/library/logging.html - # ----------------------------------------------------------------------- # - log = logging.getLogger("pymodbus") - log.setLevel(logging.ERROR) - handlers = [ - Handlers.RotatingFileHandler("logfile", maxBytes=1024 * 1024), - Handlers.SMTPHandler( - "mx.host.com", "pymodbus@host.com", ["support@host.com"], "Pymodbus" - ), - Handlers.SysLogHandler(facility="daemon"), - Handlers.DatagramHandler("localhost", 12345), - ] - [log.addHandler(h) for h in handlers] # pylint: disable=expression-not-assigned diff --git a/examples/v2.5.3/modbus_mapper.py b/examples/v2.5.3/modbus_mapper.py deleted file mode 100644 index 44f84b47c..000000000 --- a/examples/v2.5.3/modbus_mapper.py +++ /dev/null @@ -1,399 +0,0 @@ -# pylint: disable=missing-type-doc -r"""This is used to generate decoder blocks. - -so that non-programmers can define the -register values and then decode a modbus device all -without having to write a line of code for decoding. - -Currently supported formats are: - -* csv -* json -* xml - -Here is an example of generating and using a mapping decoder -(note that this is still in the works and will be greatly -simplified in the final api; it is just an example of the -requested functionality):: - - CSV: - address,type,size,name,function - 0,int16,1,Comm. count PLC,hr - 1,int16,1,Comm. count PLC,hr - 2,int16,1,Comm. count PLC,hr - 3,int16,1,Comm. count PLC,hr - 4,int16,1,Comm. count PLC,hr - 5,int16,1,Comm. count PLC,hr - 6,int16,1,Comm. count PLC,hr - 7,int16,1,Comm. count PLC,hr - 8,int16,1,Comm. count PLC,hr - 9,int16,1,Comm. count PLC,hr - 10,int32,2,Comm. count PLC,hr - 12,int32,2,Comm. count PLC,hr - - from modbus_mapper import csv_mapping_parser - from modbus_mapper import mapping_decoder - from pymodbus.client import ModbusTcpClient - from pymodbus.payload import BinaryPayloadDecoder - from pymodbus.constants import Endian - - from pprint import pprint - import logging - - # FORMAT = "%(asctime)-15s %(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s" - # logging.basicConfig(format=FORMAT) - # _logger = logging.getLogger() - # _logger.setLevel(logging.DEBUG) - - template = ["address", "type", "size", "name", "function"] - raw_mapping = csv_mapping_parser("simple_mapping_client.csv", template) - # raw_mapping = csv_mapping_parser("Naust_Comm_to_scr_client.csv", template) - mapping = mapping_decoder(raw_mapping) - - client = ModbusTcpClient(host="localhost", port=5020) - - response = client.read_holding_registers(address=int(0), count=14) - decoder = BinaryPayloadDecoder.fromRegisters( - response.registers, byteorder=Endian.Big, wordorder=Endian.Little - ) - - for block in mapping.items(): - for mac in block: - if type(mac) == dict: - # response = client.read_holding_registers( - # address=int(mac["address"]), count=mac["size"] - # ) - # decoder = BinaryPayloadDecoder.fromRegisters( - # response.registers, byteorder=Endian.Big, wordorder=Endian.Little - # ) - print("[{}]\t{}".format(mac["address"], mac["type"]()(decoder))) - # decoder._payload # remove mac["size"] bytes from beginning - -Also, using the same input mapping parsers, we can generate -populated slave contexts that can be run behind a modbus server:: - - CSV: - address,value,function,name,description - 0,0,hr,Comm. count PLC,Comm. count PLC - 1,10,hr,Comm. count PLC,Comm. count PLC - 2,20,hr,Comm. count PLC,Comm. count PLC - 3,30,hr,Comm. count PLC,Comm. count PLC - 4,40,hr,Comm. count PLC,Comm. count PLC - 5,50,hr,Comm. count PLC,Comm. count PLC - 6,60,hr,Comm. count PLC,Comm. count PLC - 7,70,hr,Comm. count PLC,Comm. count PLC - 8,80,hr,Comm. count PLC,Comm. count PLC - 9,90,hr,Comm. count PLC,Comm. count PLC - 10,100,hr,Comm. count PLC,Comm. count PLC - 11,0,hr,Comm. count PLC,Comm. count PLC - 12,120,hr,Comm. count PLC,Comm. count PLC - 13,0,hr,Comm. count PLC,Comm. count PLC - - from modbus_mapper import csv_mapping_parser - from modbus_mapper import modbus_context_decoder - - from pymodbus.server import StartTcpServer - from pymodbus.datastore.context import ModbusServerContext - from pymodbus.device import ModbusDeviceIdentification - from pymodbus.version import version - - - from pprint import pprint - import logging - - FORMAT = "%(asctime)-15s %(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s" - logging.basicConfig(format=FORMAT) - _logger = logging.getLogger() - _logger.setLevel(logging.DEBUG) - - template = ["address", "value", "function", "name", "description"] - raw_mapping = csv_mapping_parser("simple_mapping_server.csv", template) - - slave_context = modbus_context_decoder(raw_mapping) - context = ModbusServerContext(slaves=slave_context, single=True) - identity = ModbusDeviceIdentification( - info_name={ - "VendorName": "Pymodbus", - "ProductCode": "PM", - "VendorUrl": "https://github.com/pymodbus-dev/pymodbus/", - "ProductName": "Pymodbus Server", - "ModelName": "Pymodbus Server", - "MajorMinorRevision": version.short(), - } - ) - StartTcpServer(context=context, identity=identity, address=("localhost", 5020)) - -""" -import csv -import json -from collections import defaultdict -from io import StringIO -from tokenize import generate_tokens - -from pymodbus.datastore import ModbusSlaveContext, ModbusSparseDataBlock - - -# --------------------------------------------------------------------------- # -# raw mapping input parsers -# --------------------------------------------------------------------------- # -# These generate the raw mapping_blocks from some form of input -# which can then be passed to the decoder in question to supply -# the requested output result. -# --------------------------------------------------------------------------- # - - -def csv_mapping_parser(path, template): - """Given a csv file of the the mapping data for a modbus device, - - return a mapping layout that can be used to decode an new block. - - .. note:: For the template, a few values are required - to be defined: address, size, function, and type. All the remaining - values will be stored, but not formatted by the application. - So for example:: - - template = ["address", "type", "size", "name", "function"] - mappings = json_mapping_parser("mapping.json", template) - - :param path: The path to the csv input file - :param template: The row value template - :returns: The decoded csv dictionary - """ - mapping_blocks = defaultdict(dict) - with open(path, "r") as handle: # pylint: disable=unspecified-encoding - reader = csv.reader(handle) - next(reader) # skip the csv header - for row in reader: - mapping = dict(zip(template, row)) - # mapping.pop("function") - aid = mapping["address"] - mapping_blocks[aid] = mapping - return mapping_blocks - - -def json_mapping_parser(path, template): - """Given a json file of the the mapping data for a modbus device, - - return a mapping layout that can - be used to decode an new block. - - .. note:: For the template, a few values are required - to be mapped: address, size, and type. All the remaining - values will be stored, but not formatted by the application. - So for example:: - - template = { - "Start": "address", - "DataType": "type", - "Length": "size" - # the remaining keys will just pass through - } - mappings = json_mapping_parser("mapping.json", template) - - :param path: The path to the csv input file - :param template: The row value template - :returns: The decoded csv dictionary - """ - mapping_blocks = {} - with open(path, "r") as handle: # pylint: disable=unspecified-encoding - for tid, rows in json.load(handle).iteritems(): - mappings = {} - for key, values in rows.iteritems(): - mapping = {template.get(k, k): v for k, v in values.iteritems()} - mappings[int(key)] = mapping - mapping_blocks[tid] = mappings - return mapping_blocks - - -def xml_mapping_parser(): - """Given an xml file of the the mapping data for a modbus device, - - return a mapping layout that can be used to decode an new block. - - :returns: The decoded csv dictionary - """ - - -# --------------------------------------------------------------------------- # -# modbus context decoders -# --------------------------------------------------------------------------- # -# These are used to decode a raw mapping_block into a slave context with -# populated function data blocks. -# --------------------------------------------------------------------------- # -def modbus_context_decoder(mapping_blocks): - """Generate a backing slave context with initialized data blocks. - - .. note:: This expects the following for each block: - address, value, and function where function is one of - di (discretes), co (coils), hr (holding registers), or - ir (input registers). - - :param mapping_blocks: The mapping blocks - :returns: The initialized modbus slave context - """ - sparse = ModbusSparseDataBlock() - sparse.create() - for block in mapping_blocks.items(): - for mapping in block: - if type(mapping) == dict: - value = mapping["value"] - address = mapping["address"] - sparse.setValues(address=int(address), values=int(value)) - return ModbusSlaveContext( - di=sparse, co=sparse, hr=sparse, ir=sparse, zero_mode=True - ) - - -# --------------------------------------------------------------------------- # -# modbus mapping decoder -# --------------------------------------------------------------------------- # -# These are used to decode a raw mapping_block into a request decoder. -# So this allows one to simply grab a number of registers, and then -# pass them to this decoder which will do the rest. -# --------------------------------------------------------------------------- # -class ModbusTypeDecoder: - """This is a utility to determine the correct decoder to use given a type name. - - By default this supports all the types available in the default modbus - decoder, however this can easily be extended this class - and adding new types to the mapper:: - - class CustomTypeDecoder(ModbusTypeDecoder): - def __init__(self): - ModbusTypeDecode.__init__(self) - self.mapper["type-token"] = self.callback - - def parse_my_bitfield(self, tokens): - return lambda d: d.decode_my_type() - - """ - - def __init__(self): - """Initialize a new instance of the decoder""" - self.default = lambda m: self.parse_16bit_uint - self.parsers = { - "uint": self.parse_16bit_uint, - "uint8": self.parse_8bit_uint, - "uint16": self.parse_16bit_uint, - "uint32": self.parse_32bit_uint, - "uint64": self.parse_64bit_uint, - "int": self.parse_16bit_int, - "int8": self.parse_8bit_int, - "int16": self.parse_16bit_int, - "int32": self.parse_32bit_int, - "int64": self.parse_64bit_int, - "float": self.parse_32bit_float, - "float32": self.parse_32bit_float, - "float64": self.parse_64bit_float, - "string": self.parse_32bit_int, - "bits": self.parse_bits, - } - - # ------------------------------------------------------------ # - # Type parsers - # ------------------------------------------------------------ # - @staticmethod - def parse_string(tokens): - """Parse value.""" - _ = next(tokens) - size = int(next(tokens)) - return lambda d: d.decode_string(size=size) - - @staticmethod - def parse_bits(): - """Parse value.""" - return lambda d: d.decode_bits() - - @staticmethod - def parse_8bit_uint(): - """Parse value.""" - return lambda d: d.decode_8bit_uint() - - @staticmethod - def parse_16bit_uint(): - """Parse value.""" - return lambda d: d.decode_16bit_uint() - - @staticmethod - def parse_32bit_uint(): - """Parse value.""" - return lambda d: d.decode_32bit_uint() - - @staticmethod - def parse_64bit_uint(): - """Parse value.""" - return lambda d: d.decode_64bit_uint() - - @staticmethod - def parse_8bit_int(): - """Parse value.""" - return lambda d: d.decode_8bit_int() - - @staticmethod - def parse_16bit_int(): - """Parse value.""" - return lambda d: d.decode_16bit_int() - - @staticmethod - def parse_32bit_int(): - """Parse value.""" - return lambda d: d.decode_32bit_int() - - @staticmethod - def parse_64bit_int(): - """Parse value.""" - return lambda d: d.decode_64bit_int() - - @staticmethod - def parse_32bit_float(): - """Parse value.""" - return lambda d: d.decode_32bit_float() - - @staticmethod - def parse_64bit_float(): - """Parse value.""" - return lambda d: d.decode_64bit_float() - - # ------------------------------------------------------------ - # Public Interface - # ------------------------------------------------------------ - def tokenize(self, value): - """Return the tokens - - :param value: The value to tokenize - :returns: A token generator - """ - tokens = generate_tokens(StringIO(value).readline) - for _, tokval, _, _, _ in tokens: - yield tokval - - def parse(self, value): - """Return a function that supplied with a decoder, - - will decode the correct value. - - :param value: The type of value to parse - :returns: The decoder method to use - """ - tokens = self.tokenize(value) - token = next(tokens).lower() # pylint: disable=no-member - parser = self.parsers.get(token, self.default) - return parser - - -def mapping_decoder(mapping_blocks, decoder=None): - """Convert them into modbus value decoder map. - - :param mapping_blocks: The mapping blocks - :param decoder: The type decoder to use - """ - decoder = decoder or ModbusTypeDecoder() - map = defaultdict(dict) - for block in mapping_blocks.items(): - for mapping in block: - if type(mapping) == dict: - mapping["address"] = mapping["address"] - mapping["size"] = mapping["size"] - mapping["type"] = decoder.parse(mapping["type"]) - map[mapping["address"]] = mapping - return map diff --git a/examples/v2.5.3/modbus_saver.py b/examples/v2.5.3/modbus_saver.py deleted file mode 100644 index 60c1a0785..000000000 --- a/examples/v2.5.3/modbus_saver.py +++ /dev/null @@ -1,220 +0,0 @@ -"""These are a collection of helper methods. - -that can be used to save a modbus server context to file for backup, -checkpointing, or any other purpose. There use is very -simple:: - - context = server.context - saver = JsonDatastoreSaver(context) - saver.save() - -These can then be re-opened by the parsers in the -modbus_mapping module. At the moment, the supported -output formats are: - -* csv -* json -* xml - -To implement your own, simply subclass ModbusDatastoreSaver -and supply the needed callbacks for your given format: - -* handle_store_start(self, store) -* handle_store_end(self, store) -* handle_slave_start(self, slave) -* handle_slave_end(self, slave) -* handle_save_start(self) -* handle_save_end(self) -""" -import json -import xml.etree.ElementTree as xml - - -class ModbusDatastoreSaver: - """An abstract base class. - - that can be used to implement - a persistence format for the modbus server context. In - order to use it, just complete the necessary callbacks - (SAX style) that your persistence format needs. - """ - - def __init__(self, context, path=None): - """Initialize a new instance of the saver. - - :param context: The modbus server context - :param path: The output path to save to - """ - self.context = context - self.path = path or "modbus-context-dump" - - def save(self): - """Save the context to file. - - which calls the various callbacks which the sub classes will implement. - """ - with open( # pylint: disable=unspecified-encoding - self.path, "w" - ) as self.file_handle: # pylint: disable=attribute-defined-outside-init - self.handle_save_start() - for slave_name, slave in self.context: - self.handle_slave_start(slave_name) - for store_name, store in slave.store.iteritems(): - self.handle_store_start(store_name) - self.handle_store_values(iter(store)) # pylint: disable=no-member - self.handle_store_end(store_name) - self.handle_slave_end(slave_name) - self.handle_save_end() - - # ------------------------------------------------------------ - # predefined state machine callbacks - # ------------------------------------------------------------ - def handle_save_start(self): - """Handle save start.""" - - def handle_store_start(self, store): - """Handle store start.""" - - def handle_store_end(self, store): - """Handle store end.""" - - def handle_slave_start(self, slave): - """Handle slave start.""" - - def handle_slave_end(self, slave): - """Handle slave end.""" - - def handle_save_end(self): - """Handle save end.""" - - -# ---------------------------------------------------------------- # -# Implementations of the data store savers -# ---------------------------------------------------------------- # -class JsonDatastoreSaver(ModbusDatastoreSaver): - """An implementation of the modbus datastore saver. - - that persists the context as a json document. - """ - - _context = None - _store = None - _slave = None - - STORE_NAMES = { - "i": "input-registers", - "d": "discretes", - "h": "holding-registers", - "c": "coils", - } - - def handle_save_start(self): - """Handle save start.""" - self._context = {} - - def handle_slave_start(self, slave): - """Handle slave start.""" - self._context[hex(slave)] = self._slave = {} - - def handle_store_start(self, store): - """Handle store start.""" - self._store = self.STORE_NAMES[store] - - def handle_store_values(self, values): - """Handle store values.""" - self._slave[self._store] = dict(values) - - def handle_save_end(self): - """Handle save end.""" - json.dump(self._context, self.file_handle) - - -class CsvDatastoreSaver(ModbusDatastoreSaver): - """An implementation of the modbus datastore saver. - - that persists the context as a csv document. - """ - - _context = None - _store = None - _line = None - NEWLINE = "\r\n" - HEADER = "slave,store,address,value" + NEWLINE - STORE_NAMES = { - "i": "i", - "d": "d", - "h": "h", - "c": "c", - } - - def handle_save_start(self): - """Handle save start.""" - self.file_handle.write(self.HEADER) - - def handle_slave_start(self, slave): - """Handle slave start.""" - self._line = [str(slave)] - - def handle_store_start(self, store): - """Handle store start.""" - self._line.append(self.STORE_NAMES[store]) - - def handle_store_values(self, values): - """Handle store values.""" - self.file_handle.writelines(self.handle_store_value(values)) - - def handle_store_end(self, store): - """Handle store end.""" - self._line.pop() - - def handle_store_value(self, values): - """Handle store value.""" - for val_a, val_v in values: - yield ",".join(self._line + [str(val_a), str(val_v)]) + self.NEWLINE - - -class XmlDatastoreSaver(ModbusDatastoreSaver): - """An implementation of the modbus datastore saver. - - that persists the context as a XML document. - """ - - _context = None - _store = None - - STORE_NAMES = { - "i": "input-registers", - "d": "discretes", - "h": "holding-registers", - "c": "coils", - } - - def handle_save_start(self): - """Handle save start.""" - self._context = xml.Element("context") - self._root = xml.ElementTree( # pylint: disable=attribute-defined-outside-init - self._context - ) - - def handle_slave_start(self, slave): - """Handle slave start.""" - self._slave = xml.SubElement( # pylint: disable=attribute-defined-outside-init - self._context, "slave" - ) - self._slave.set("id", str(slave)) - - def handle_store_start(self, store): - """Handle store start.""" - self._store = xml.SubElement(self._slave, "store") - self._store.set("function", self.STORE_NAMES[store]) - - def handle_store_values(self, values): - """Handle store values.""" - for address, value in values: - entry = xml.SubElement(self._store, "entry") - entry.text = str(value) - entry.set("address", str(address)) - - def handle_save_end(self): - """Handle save end.""" - self._root.write(self.file_handle) diff --git a/examples/v2.5.3/modbus_simulator.py b/examples/v2.5.3/modbus_simulator.py deleted file mode 100644 index 4dc174422..000000000 --- a/examples/v2.5.3/modbus_simulator.py +++ /dev/null @@ -1,146 +0,0 @@ -#!/usr/bin/env python3 -# pylint: disable=missing-raises-doc -"""An example of creating a fully implemented modbus server. - -with read/write data as well as user configurable base data -""" -import logging -import pickle -from optparse import OptionParser # pylint: disable=deprecated-module - -from pymodbus.datastore import ModbusServerContext, ModbusSlaveContext -from pymodbus.server import StartTcpServer - - -# -------------------------------------------------------------------------- # -# Logging -# -------------------------------------------------------------------------- # -server_log = logging.getLogger("pymodbus.server") -protocol_log = logging.getLogger("pymodbus.protocol") -_logger = logging.getLogger(__name__) - -# -------------------------------------------------------------------------- # -# Extra Global Functions -# -------------------------------------------------------------------------- # -# These are extra helper functions that don't belong in a class -# -------------------------------------------------------------------------- # -# import getpass - - -def root_test(): - """Check to see if we are running as root""" - return True # removed for the time being as it isn't portable - # return getpass.getuser() == "root" - - -# -------------------------------------------------------------------------- # -# Helper Classes -# -------------------------------------------------------------------------- # - - -class ConfigurationException(Exception): - """Exception for configuration error""" - - def __init__(self, string): - """Initialize the ConfigurationException instance - - :param string: The message to append to the exception - """ - Exception.__init__(self, string) - self.string = string - - def __str__(self): - """Build a representation of the object - - :returns: A string representation of the object - """ - return f"Configuration Error: {self.string}" - - -class Configuration: # pylint: disable=too-few-public-methods - """Class used to parse configuration file and create and modbus datastore. - - The format of the configuration file is actually just a - python pickle, which is a compressed memory dump from - the scraper. - """ - - def __init__(self, config): - """Try to load a configuration file. - - lets the file not found exception fall through - - :param config: The pickled datastore - """ - try: - self.file = open(config, "rb") # pylint: disable=consider-using-with - except Exception as exc: - _logger.critical(str(exc)) - raise ConfigurationException( # pylint: disable=raise-missing-from - f"File not found {config}" - ) - - def parse(self): - """Parse the config file and creates a server context""" - handle = pickle.load(self.file) - try: # test for existence, or bomb - dsd = handle["di"] - csd = handle["ci"] - hsd = handle["hr"] - isd = handle["ir"] - except Exception: - raise ConfigurationException( # pylint: disable=raise-missing-from - "Invalid Configuration" - ) - slave = ModbusSlaveContext(d=dsd, c=csd, h=hsd, i=isd) - return ModbusServerContext(slaves=slave) - - -# -------------------------------------------------------------------------- # -# Main start point -# -------------------------------------------------------------------------- # - - -def main(): - """Server launcher""" - parser = OptionParser() - parser.add_option( - "-c", "--conf", help="The configuration file to load", dest="file" - ) - parser.add_option( - "-D", - "--debug", - help="Turn on to enable tracing", - action="store_true", - dest="debug", - default=False, - ) - (opt, _) = parser.parse_args() - - # enable debugging information - if opt.debug: - try: - server_log.setLevel(logging.DEBUG) - protocol_log.setLevel(logging.DEBUG) - except Exception: # pylint: disable=broad-except - print("Logging is not supported on this system") - - # parse configuration file and run - try: - conf = Configuration(opt.file) - StartTcpServer(context=conf.parse()) - except ConfigurationException as err: - print(err) - parser.print_help() - - -# -------------------------------------------------------------------------- # -# Main jumper -# -------------------------------------------------------------------------- # - - -if __name__ == "__main__": - if root_test(): - main() - else: - print("This script must be run as root!") diff --git a/examples/v2.5.3/modbus_tls_client.py b/examples/v2.5.3/modbus_tls_client.py deleted file mode 100755 index 042cf61b9..000000000 --- a/examples/v2.5.3/modbus_tls_client.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python3 -"""Simple Modbus TCP over TLS client. - -This is a simple example of writing a modbus TCP over TLS client that uses -Python builtin module ssl - TLS/SSL wrapper for socket objects for the TLS -feature. -""" -# -------------------------------------------------------------------------- # -# import necessary libraries -# -------------------------------------------------------------------------- # -import ssl - -from pymodbus.client import ModbusTlsClient - - -# -------------------------------------------------------------------------- # -# the TLS detail security can be set in SSLContext which is the context here -# -------------------------------------------------------------------------- # -sslctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) -sslctx.verify_mode = ssl.CERT_REQUIRED -sslctx.check_hostname = True - -# Prepare client's certificate which the server requires for TLS full handshake -# sslctx.load_cert_chain(certfile="client.crt", keyfile="client.key", -# password="pwd") - -# -------------------------------------------------------------------------- # -# pass SSLContext which is the context here to ModbusTcpClient() -# -------------------------------------------------------------------------- # -client = ModbusTlsClient("test.host.com", 8020, sslctx=sslctx) -client.connect() - -result = client.read_coils(1, 8) -print(result.bits) - -client.close() diff --git a/examples/v2.5.3/modicon_payload.py b/examples/v2.5.3/modicon_payload.py deleted file mode 100644 index 07845047b..000000000 --- a/examples/v2.5.3/modicon_payload.py +++ /dev/null @@ -1,273 +0,0 @@ -# pylint: disable=missing-type-doc,missing-raises-doc -"""Modbus Modicon Payload Builder. - -This is an example of building a custom payload builder -that can be used in the pymodbus library. Below is a -simple modicon encoded builder and decoder. -""" -from struct import pack, unpack - -from pymodbus.constants import Endian -from pymodbus.exceptions import ParameterException -from pymodbus.utilities import pack_bitstring, unpack_bitstring - - -class ModiconPayloadBuilder: - """A utility that helps build modicon encoded payload messages. - - to be written with the various modbus messages. - example:: - - builder = ModiconPayloadBuilder() - builder.add_8bit_uint(1) - builder.add_16bit_uint(2) - payload = builder.build() - """ - - def __init__(self, payload=None, endian=Endian.Little): - """Initialize a new instance of the payload builder - - :param payload: Raw payload data to initialize with - :param endian: The endianness of the payload - """ - self._payload = payload or [] - self._endian = endian - - def __str__(self): - """Return the payload buffer as a string - - :returns: The payload buffer as a string - """ - return "".join(self._payload) - - def reset(self): - """Reset the payload buffer""" - self._payload = [] - - def build(self): - """Return the payload buffer as a list - - This list is two bytes per element and can - thus be treated as a list of registers. - - :returns: The payload buffer as a list - """ - string = str(self) - length = len(string) - string = string + ("\x00" * (length % 2)) - return [string[i : i + 2] for i in range(0, length, 2)] - - def add_bits(self, values): - """Add a collection of bits to be encoded - - If these are less than a multiple of eight, - they will be left padded with 0 bits to make - it so. - - :param values: The value to add to the buffer - """ - value = pack_bitstring(values) - self._payload.append(value) - - def add_8bit_uint(self, value): - """Add a 8 bit unsigned int to the buffer - - :param value: The value to add to the buffer - """ - fstring = self._endian + "B" - self._payload.append(pack(fstring, value)) - - def add_16bit_uint(self, value): - """Add a 16 bit unsigned int to the buffer - - :param value: The value to add to the buffer - """ - fstring = self._endian + "H" - self._payload.append(pack(fstring, value)) - - def add_32bit_uint(self, value): - """Add a 32 bit unsigned int to the buffer - - :param value: The value to add to the buffer - """ - fstring = self._endian + "I" - handle = pack(fstring, value) - handle = handle[2:] + handle[:2] - self._payload.append(handle) - - def add_8bit_int(self, value): - """Add a 8 bit signed int to the buffer - - :param value: The value to add to the buffer - """ - fstring = self._endian + "b" - self._payload.append(pack(fstring, value)) - - def add_16bit_int(self, value): - """Add a 16 bit signed int to the buffer - - :param value: The value to add to the buffer - """ - fstring = self._endian + "h" - self._payload.append(pack(fstring, value)) - - def add_32bit_int(self, value): - """Add a 32 bit signed int to the buffer - - :param value: The value to add to the buffer - """ - fstring = self._endian + "i" - handle = pack(fstring, value) - handle = handle[2:] + handle[:2] - self._payload.append(handle) - - def add_32bit_float(self, value): - """Add a 32 bit float to the buffer - - :param value: The value to add to the buffer - """ - fstring = self._endian + "f" - handle = pack(fstring, value) - handle = handle[2:] + handle[:2] - self._payload.append(handle) - - def add_string(self, value): - """Add a string to the buffer - - :param value: The value to add to the buffer - """ - fstring = self._endian + "s" - for i in value: - self._payload.append(pack(fstring, i)) - - -class ModiconPayloadDecoder: - """A utility that helps decode modicon encoded payload messages from a modbus response message. - - What follows is a simple example:: - - decoder = ModiconPayloadDecoder(payload) - first = decoder.decode_8bit_uint() - second = decoder.decode_16bit_uint() - """ - - def __init__(self, payload, endian): - """Initialize a new payload decoder - - :param payload: The payload to decode with - """ - self._payload = payload - self._pointer = 0x00 - self._endian = endian - - @staticmethod - def from_registers(registers, endian=Endian.Little): - """Initialize a payload decoder. - - with the result of reading a collection of registers from a modbus device. - - The registers are treated as a list of 2 byte values. - We have to do this because of how the data has already - been decoded by the rest of the library. - - :param registers: The register results to initialize with - :param endian: The endianness of the payload - :returns: An initialized PayloadDecoder - """ - if isinstance(registers, list): # repack into flat binary - payload = "".join(pack(">H", x) for x in registers) - return ModiconPayloadDecoder(payload, endian) - raise ParameterException("Invalid collection of registers supplied") - - @staticmethod - def from_coils(coils, endian=Endian.Little): - """Initialize a payload decoder. - - with the result of reading a collection of coils from a modbus device. - - The coils are treated as a list of bit(boolean) values. - - :param coils: The coil results to initialize with - :param endian: The endianness of the payload - :returns: An initialized PayloadDecoder - """ - if isinstance(coils, list): - payload = pack_bitstring(coils) - return ModiconPayloadDecoder(payload, endian) - raise ParameterException("Invalid collection of coils supplied") - - def reset(self): - """Reset the decoder pointer back to the start""" - self._pointer = 0x00 - - def decode_8bit_uint(self): - """Decode a 8 bit unsigned int from the buffer""" - self._pointer += 1 - fstring = self._endian + "B" - handle = self._payload[self._pointer - 1 : self._pointer] - return unpack(fstring, handle)[0] - - def decode_16bit_uint(self): - """Decode a 16 bit unsigned int from the buffer""" - self._pointer += 2 - fstring = self._endian + "H" - handle = self._payload[self._pointer - 2 : self._pointer] - return unpack(fstring, handle)[0] - - def decode_32bit_uint(self): - """Decode a 32 bit unsigned int from the buffer""" - self._pointer += 4 - fstring = self._endian + "I" - handle = self._payload[self._pointer - 4 : self._pointer] - handle = handle[2:] + handle[:2] - return unpack(fstring, handle)[0] - - def decode_8bit_int(self): - """Decode a 8 bit signed int from the buffer""" - self._pointer += 1 - fstring = self._endian + "b" - handle = self._payload[self._pointer - 1 : self._pointer] - return unpack(fstring, handle)[0] - - def decode_16bit_int(self): - """Decode a 16 bit signed int from the buffer""" - self._pointer += 2 - fstring = self._endian + "h" - handle = self._payload[self._pointer - 2 : self._pointer] - return unpack(fstring, handle)[0] - - def decode_32bit_int(self): - """Decode a 32 bit signed int from the buffer""" - self._pointer += 4 - fstring = self._endian + "i" - handle = self._payload[self._pointer - 4 : self._pointer] - handle = handle[2:] + handle[:2] - return unpack(fstring, handle)[0] - - def decode_32bit_float(self): - """Decode a float from the buffer""" - self._pointer += 4 - fstring = self._endian + "f" - handle = self._payload[self._pointer - 4 : self._pointer] - handle = handle[2:] + handle[:2] - return unpack(fstring, handle)[0] - - def decode_bits(self): - """Decode a byte worth of bits from the buffer""" - self._pointer += 1 - handle = self._payload[self._pointer - 1 : self._pointer] - return unpack_bitstring(handle) - - def decode_string(self, size=1): - """Decode a string from the buffer - - :param size: The size of the string to decode - """ - self._pointer += size - return self._payload[self._pointer - size : self._pointer] - - -# -------------------------------------------------------------------------- # -# Exported Identifiers -# -------------------------------------------------------------------------- # -__all__ = ["ModiconPayloadBuilder", "ModiconPayloadDecoder"] diff --git a/examples/v2.5.3/performance.py b/examples/v2.5.3/performance.py deleted file mode 100755 index d4a36e2a2..000000000 --- a/examples/v2.5.3/performance.py +++ /dev/null @@ -1,147 +0,0 @@ -#!/usr/bin/env python3 -# pylint: disable=missing-type-doc -"""Pymodbus Performance Example. - -The following is an quick performance check of the synchronous -modbus client. -""" -# --------------------------------------------------------------------------- # -# import the necessary modules -# --------------------------------------------------------------------------- # -import logging -import os -from concurrent.futures import ThreadPoolExecutor as eWorker -from concurrent.futures import as_completed -from threading import Lock -from threading import Thread as tWorker -from time import time - -from pymodbus.client import ModbusTcpClient - - -try: - from multiprocessing import Process as mWorker - from multiprocessing import log_to_stderr -except ImportError: - log_to_stderr = logging.getLogger - -# --------------------------------------------------------------------------- # -# choose between threads or processes -# --------------------------------------------------------------------------- # - -# from multiprocessing import Process as Worker -# from threading import Thread as Worker -_thread_lock = Lock() -# --------------------------------------------------------------------------- # -# initialize the test -# --------------------------------------------------------------------------- # -# Modify the parameters below to control how we are testing the client: -# -# * workers - the number of workers to use at once -# * cycles - the total number of requests to send -# * host - the host to send the requests to -# --------------------------------------------------------------------------- # -workers = 10 # pylint: disable=invalid-name -cycles = 1000 # pylint: disable=invalid-name -host = "127.0.0.1" # pylint: disable=invalid-name - - -# --------------------------------------------------------------------------- # -# perform the test -# --------------------------------------------------------------------------- # -# This test is written such that it can be used by many threads of processes -# although it should be noted that there are performance penalties -# associated with each strategy. -# --------------------------------------------------------------------------- # -def single_client_test(n_host, n_cycles): - """Perform a single threaded test of a synchronous client against the specified host - - :param n_host: The host to connect to - :param n_cycles: The number of iterations to perform - """ - logger = log_to_stderr() - logger.setLevel(logging.WARNING) - txt = f"starting worker: {os.getpid()}" - logger.debug(txt) - - try: - count = 0 - client = ModbusTcpClient(n_host, port=5020) - while count < n_cycles: - client.read_holding_registers(10, 123, slave=1) - count += 1 - except Exception: # pylint: disable=broad-except - logger.exception("failed to run test successfully") - txt = f"finished worker: {os.getpid()}" - logger.debug(txt) - - -def multiprocessing_test(func, extras): - """Multiprocessing test.""" - start_time = time() - procs = [mWorker(target=func, args=extras) for _ in range(workers)] - - any(p.start() for p in procs) # start the workers - any(p.join() for p in procs) # wait for the workers to finish - return start_time - - -def thread_test(func, extras): - """Thread test.""" - start_time = time() - procs = [tWorker(target=func, args=extras) for _ in range(workers)] - - any(p.start() for p in procs) # start the workers - any(p.join() for p in procs) # wait for the workers to finish - return start_time - - -def thread_pool_exe_test(func, extras): - """Thread pool exe.""" - start_time = time() - with eWorker(max_workers=workers, thread_name_prefix="Perform") as exe: - futures = {exe.submit(func, *extras): job for job in range(workers)} - for future in as_completed(futures): - future.result() - return start_time - - -# --------------------------------------------------------------------------- # -# run our test and check results -# --------------------------------------------------------------------------- # -# We shard the total number of requests to perform between the number of -# threads that was specified. We then start all the threads and block on -# them to finish. This may need to switch to another mechanism to signal -# finished as the process/thread start up/shut down may skew the test a bit. - -# RTU 32 requests/second @9600 -# TCP 31430 requests/second - -# --------------------------------------------------------------------------- # - - -if __name__ == "__main__": - args = (host, int(cycles * 1.0 / workers)) - # with Worker(max_workers=workers, thread_name_prefix="Perform") as exe: - # futures = {exe.submit(single_client_test, *args): job for job in range(workers)} - # for future in as_completed(futures): - # data = future.result() - # for _ in range(workers): - # futures.append(Worker.submit(single_client_test, args=args)) - # procs = [Worker(target=single_client_test, args=args) - # for _ in range(workers)] - - # any(p.start() for p in procs) # start the workers - # any(p.join() for p in procs) # wait for the workers to finish - # start = multiprocessing_test(single_client_test, args) - # start = thread_pool_exe_test(single_client_test, args) - for tester in (multiprocessing_test, thread_test, thread_pool_exe_test): - print(tester.__name__) - start = tester(single_client_test, args) - stop = time() - print(f"{(1.0 * cycles) / (stop - start)} requests/second") - print( - f"time taken to complete {cycles} cycle by " - f"{workers} workers is {stop - start} seconds" - ) - print() diff --git a/examples/v2.5.3/remote_server_context.py b/examples/v2.5.3/remote_server_context.py deleted file mode 100644 index c3981cfad..000000000 --- a/examples/v2.5.3/remote_server_context.py +++ /dev/null @@ -1,229 +0,0 @@ -# pylint: disable=missing-type-doc,missing-param-doc,differing-param-doc,missing-raises-doc -"""Although there is a remote server context already in the main library, - -it works under the assumption that users would have a server context -of the following form:: - - server_context = { - 0x00: client("host1.something.com"), - 0x01: client("host2.something.com"), - 0x02: client("host3.something.com") - } - -This example is how to create a server context where the client is -pointing to the same host, but the requested slave id is used as the -slave for the client:: - - server_context = { - 0x00: client("host1.something.com", 0x00), - 0x01: client("host1.something.com", 0x01), - 0x02: client("host1.something.com", 0x02) - } -""" -import logging - -from pymodbus.datastore import ModbusBaseSlaveContext -from pymodbus.exceptions import NotImplementedException - - -# -------------------------------------------------------------------------- # -# Logging -# -------------------------------------------------------------------------- # -_logger = logging.getLogger(__name__) - -# -------------------------------------------------------------------------- # -# Slave Context -# -------------------------------------------------------------------------- # -# Basically we create a new slave context for the given slave identifier so -# that this slave context will only make requests to that slave with the -# client that the server is maintaining. -# -------------------------------------------------------------------------- # - - -class RemoteSingleSlaveContext(ModbusBaseSlaveContext): - """This is a remote server context, - - that allows one to create a server context backed by a single client that - may be attached to many slave units. This can be used to - effectively create a modbus forwarding server. - """ - - def __init__(self, context, unit_id): - """Initialize the datastores - - :param context: The underlying context to operate with - :param unit_id: The slave that this context will contact - """ - self.context = context - self.unit_id = unit_id - - def reset(self): - """Reset all the datastores to their default values""" - raise NotImplementedException() - - def validate(self, fx, address, count=1): - """Validate the request to make sure it is in range - - :param fx: The function we are working with - :param address: The starting address - :param count: The number of values to test - :returns: True if the request in within range, False otherwise - """ - txt = f"validate[{fx}] {address}:{count}" - _logger.debug(txt) - result = self.context.get_callbacks[self.decode(fx)]( - address, count, self.unit_id - ) - return not result.isError() - - def getValues(self, fx, address, count=1): - """Get `count` values from datastore - - :param fx: The function we are working with - :param address: The starting address - :param count: The number of values to retrieve - :returns: The requested values from a:a+c - """ - txt = f"get values[{fx}] {address}:{count}" - _logger.debug(txt) - result = self.context.get_callbacks[self.decode(fx)]( - address, count, self.unit_id - ) - return self.__extract_result(self.decode(fx), result) - - def setValues(self, fx, address, values): - """Set the datastore with the supplied values - - :param fx: The function we are working with - :param address: The starting address - :param values: The new values to be set - """ - txt = f"set values[{fx}] {address}:{len(values)}" - _logger.debug(txt) - self.context.set_callbacks[self.decode(fx)](address, values, self.unit_id) - - def __str__(self): - """Return a string representation of the context - - :returns: A string representation of the context - """ - return f"Remote Single Slave Context({self.unit_id})" - - def __extract_result(self, f_code, result): - """Extract the values out of a response. - - The future api should make the result consistent so we can just call `result.getValues()`. - - :param fx: The function to call - :param result: The resulting data - """ - if not result.isError(): - if f_code in {"d", "c"}: - return result.bits - if f_code in {"h", "i"}: - return result.registers - return None - return result - - -# -------------------------------------------------------------------------- # -# Server Context -# -------------------------------------------------------------------------- # -# Think of this as simply a dictionary of { unit_id: client(req, unit_id) } -# -------------------------------------------------------------------------- # - - -class RemoteServerContext: - """This is a remote server context, - - that allows one to create a server context backed by a single client that - may be attached to many slave units. This can be used to - effectively create a modbus forwarding server. - """ - - def __init__(self, client): - """Initialize the datastores - - :param client: The client to retrieve values with - """ - self.get_callbacks = { - "d": lambda a, c, s: client.read_discrete_inputs( # pylint: disable=unnecessary-lambda - a, c, s - ), - "c": lambda a, c, s: client.read_coils( # pylint: disable=unnecessary-lambda - a, c, s - ), - "h": lambda a, c, s: client.read_holding_registers( # pylint: disable=unnecessary-lambda - a, c, s - ), - "i": lambda a, c, s: client.read_input_registers( # pylint: disable=unnecessary-lambda - a, c, s - ), - } - self.set_callbacks = { - "d": lambda a, v, s: client.write_coils( # pylint: disable=unnecessary-lambda - a, v, s - ), - "c": lambda a, v, s: client.write_coils( # pylint: disable=unnecessary-lambda - a, v, s - ), - "h": lambda a, v, s: client.write_registers( # pylint: disable=unnecessary-lambda - a, v, s - ), - "i": lambda a, v, s: client.write_registers( # pylint: disable=unnecessary-lambda - a, v, s - ), - } - self._client = client - self.slaves = {} # simply a cache - - def __str__(self): - """Return a string representation of the context - - :returns: A string representation of the context - """ - return f"Remote Server Context{self._client}" - - def __iter__(self): - """Iterate over the current collection of slave contexts. - - :returns: An iterator over the slave contexts - """ - # note, this may not include all slaves - return iter(self.slaves.items()) - - def __contains__(self, slave): - """Check if the given slave is in this list - - :param slave: slave The slave to check for existence - :returns: True if the slave exists, False otherwise - """ - # we don't want to check the cache here as the - # slave may not exist yet or may not exist any - # more. The best thing to do is try and fail. - return True - - def __setitem__(self, slave, context): - """Use to set a new slave context - - :param slave: The slave context to set - :param context: The new context to set for this slave - """ - raise NotImplementedException() # doesn't make sense here - - def __delitem__(self, slave): - """Use to access the slave context - - :param slave: The slave context to remove - """ - raise NotImplementedException() # doesn't make sense here - - def __getitem__(self, slave): - """Use to get access to a slave context - - :param slave: The slave context to get - :returns: The requested slave context - """ - if slave not in self.slaves: - self.slaves[slave] = RemoteSingleSlaveContext(self, slave) - return self.slaves[slave] diff --git a/examples/v2.5.3/rx_messages b/examples/v2.5.3/rx_messages deleted file mode 100644 index 51fb12a56..000000000 --- a/examples/v2.5.3/rx_messages +++ /dev/null @@ -1,215 +0,0 @@ -# ------------------------------------------------------------ -# What follows is a collection of encoded messages that can -# be used to test the message-parser. Simply uncomment the -# messages you want decoded and run the message parser with -# the given arguments. What follows is the listing of messages -# that are encoded in each format: -# -# - ReadHoldingRegistersResponse -# - ReadDiscreteInputsResponse -# - ReadInputRegistersResponse -# - ReadCoilsResponse -# - WriteMultipleCoilsResponse -# - WriteMultipleRegistersResponse -# - WriteSingleRegisterResponse -# - WriteSingleCoilResponse -# - ReadWriteMultipleRegistersResponse -# - ReadExceptionStatusResponse -# - GetCommEventCounterResponse -# - GetCommEventLogResponse -# - ReportSlaveIdResponse -# - ReadFileRecordResponse -# - WriteFileRecordResponse -# - MaskWriteRegisterResponse -# - ReadFifoQueueResponse -# - ReadDeviceInformationResponse -# - ReturnQueryDataResponse -# - RestartCommunicationsOptionResponse -# - ReturnDiagnosticRegisterResponse -# - ChangeAsciiInputDelimiterResponse -# - ForceListenOnlyModeResponse -# - ClearCountersResponse -# - ReturnBusMessageCountResponse -# - ReturnBusCommunicationErrorCountResponse -# - ReturnBusExceptionErrorCountResponse -# - ReturnSlaveMessageCountResponse -# - ReturnSlaveNoReponseCountResponse -# - ReturnSlaveNAKCountResponse -# - ReturnSlaveBusyCountResponse -# - ReturnSlaveBusCharacterOverrunCountResponse -# - ReturnIopOverrunCountResponse -# - ClearOverrunCountResponse -# - GetClearModbusPlusResponse -# ------------------------------------------------------------ -# Modbus TCP Messages -# ------------------------------------------------------------ -# [ MBAP Header ] [ Function Code] [ Data ] -# [ tid ][ pid ][ length ][ uid ] -# 2b 2b 2b 1b 1b Nb -# -# ./message-parser -b -p tcp -f messages -# ------------------------------------------------------------ -#00010000001301031000010001000100010001000100010001 -#000100000004010201ff -#00010000001301041000010001000100010001000100010001 -#000100000004010101ff -#000100000006010f00120008 -#000100000006011000120008 -#000100000006010600120001 -#00010000000601050012ff00 -#00010000001301171000010001000100010001000100010001 -#000100000003010700 -#000100000006010b00000008 -#000100000009010c06000000000000 -#00010000000501110300ff -#000100000003011400 -#000100000003011500 -#00010000000801160012ffff0000 -#00010000001601180012001000010001000100010001000100010001 -#000100000008012b0e0183000000 -#000100000006010800000000 -#000100000006010800010000 -#000100000006010800020000 -#000100000006010800030000 -#00010000000401080004 -#0001000000060108000a0000 -#0001000000060108000b0000 -#0001000000060108000c0000 -#0001000000060108000d0000 -#0001000000060108000e0000 -#0001000000060108000f0000 -#000100000006010800100000 -#000100000006010800110000 -#000100000006010800120000 -#000100000006010800130000 -#000100000006010800140000 -#000100000006010800150000 -# ------------------------------------------------------------ -# Modbus RTU Messages -# ------------------------------------------------------------ -# [Address ][ Function Code] [ Data ][ CRC ] -# 1b 1b Nb 2b -# -# ./message-parser -b -p rtu -f messages -# ------------------------------------------------------------ -#0103100001000100010001000100010001000193b4 -#010201ffe1c8 -#0104100001000100010001000100010001000122c1 -#010101ff11c8 -#010f00120008f408 -#01100012000861ca -#010600120001e80f -#01050012ff002c3f -#01171000010001000100010001000100010001d640 -#0107002230 -#010b00000008a5cd -#010c060000000000006135 -#01110300ffacbc -#0114002f00 -#0115002e90 -#01160012ffff00004e21 -#01180012001000010001000100010001000100010001d74d -#012b0e01830000000faf -#010800000000e00b -#010800010000b1cb -#01080002000041cb -#010800030000100b -#0108000481d9 -#0108000a0000c009 -#0108000b000091c9 -#0108000c00002008 -#0108000d000071c8 -#0108000e000081c8 -#0108000f0000d008 -#010800100000e1ce -#010800110000b00e -#010800120000400e -#01080013000011ce -#010800140000a00f -#010800150000f1cf -# ------------------------------------------------------------ -# Modbus ASCII Messages -# ------------------------------------------------------------ -# [ Start ][Address ][ Function ][ Data ][ LRC ][ End ] -# 1c 2c 2c Nc 2c 2c -# -# ./message-parser -a -p ascii -f messages -# ------------------------------------------------------------ -#:01031000010001000100010001000100010001E4 -#:010201FFFD -#:01041000010001000100010001000100010001E3 -#:010101FFFE -#:010F00120008D6 -#:011000120008D5 -#:010600120001E6 -#:01050012FF00E9 -#:01171000010001000100010001000100010001D0 -#:010700F8 -#:010B00000008EC -#:010C06000000000000ED -#:01110300FFEC -#:011400EB -#:011500EA -#:01160012FFFF0000D9 -#:01180012001000010001000100010001000100010001BD -#:012B0E018300000042 -#:010800000000F7 -#:010800010000F6 -#:010800020000F5 -#:010800030000F4 -#:01080004F3 -#:0108000A0000ED -#:0108000B0000EC -#:0108000C0000EB -#:0108000D0000EA -#:0108000E0000E9 -#:0108000F0000E8 -#:010800100000E7 -#:010800110000E6 -#:010800120000E5 -#:010800130000E4 -#:010800140000E3 -#:010800150000E2 -# ------------------------------------------------------------ -# Modbus Binary Messages -# ------------------------------------------------------------ -# [ Start ][Address ][ Function ][ Data ][ CRC ][ End ] -# 1b 1b 1b Nb 2b 1b -# -# ./message-parser -b -p binary -f messages -# ------------------------------------------------------------ -#7b0103100001000100010001000100010001000193b47d -#7b010201ffe1c87d -#7b0104100001000100010001000100010001000122c17d -#7b010101ff11c87d -#7b010f00120008f4087d -#7b01100012000861ca7d -#7b010600120001e80f7d -#7b01050012ff002c3f7d -#7b01171000010001000100010001000100010001d6407d -#7b01070022307d -#7b010b00000008a5cd7d -#7b010c0600000000000061357d -#7b01110300ffacbc7d -#7b0114002f007d -#7b0115002e907d -#7b01160012ffff00004e217d -#7b01180012001000010001000100010001000100010001d74d7d -#7b012b0e01830000000faf7d -#7b010800000000e00b7d -#7b010800010000b1cb7d -#7b01080002000041cb7d -#7b010800030000100b7d -#7b0108000481d97d -#7b0108000a0000c0097d -#7b0108000b000091c97d -#7b0108000c000020087d -#7b0108000d000071c87d -#7b0108000e000081c87d -#7b0108000f0000d0087d -#7b010800100000e1ce7d -#7b010800110000b00e7d -#7b010800120000400e7d -#7b01080013000011ce7d -#7b010800140000a00f7d -#7b010800150000f1cf7d diff --git a/examples/v2.5.3/thread_safe_datastore.py b/examples/v2.5.3/thread_safe_datastore.py deleted file mode 100644 index cb085fb7e..000000000 --- a/examples/v2.5.3/thread_safe_datastore.py +++ /dev/null @@ -1,229 +0,0 @@ -# pylint: disable=missing-type-doc -"""Thread safe datastore.""" -import threading -from contextlib import contextmanager - -from pymodbus.datastore.store import BaseModbusDataBlock - - -class ContextWrapper: - """This is a simple wrapper around enter and exit functions - - that conforms to the python context manager protocol: - - with ContextWrapper(enter, leave): - do_something() - """ - - def __init__(self, enter=None, leave=None, factory=None): - """Initialize.""" - self._enter = enter - self._leave = leave - self._factory = factory - - def __enter__(self): - """Do on enter.""" - if self.enter: # pylint: disable=no-member - self._enter() - return self if not self._factory else self._factory() - - def __exit__(self, *args): - """Do on exit.""" - if self._leave: - self._leave() - - -class ReadWriteLock: - """This reader writer lock guarantees write order, - - but not read order and is generally biased towards allowing writes - if they are available to prevent starvation. - TODO: - * allow user to choose between read/write/random biasing - - currently write biased - - read biased allow N readers in queue - - random is 50/50 choice of next - """ - - def __init__(self): - """Initialize a new instance of the ReadWriteLock""" - self.queue = [] # the current writer queue - self.lock = threading.Lock() # the underlying condition lock - self.read_condition = threading.Condition( - self.lock - ) # the single reader condition - self.readers = 0 # the number of current readers - self.writer = False # is there a current writer - - def __is_pending_writer(self): - """Check is pending writer.""" - return self.writer or ( # if there is a current writer - self.queue # or if there is a waiting writer - and (self.queue[0] != self.read_condition) - ) - - def acquire_reader(self): - """Notify the lock that a new reader is requesting the underlying resource.""" - with self.lock: - if self.__is_pending_writer(): # if there are existing writers waiting - if ( - self.read_condition not in self.queue - ): # do not pollute the queue with readers - self.queue.append( - self.read_condition - ) # add the readers in line for the queue - while ( - self.__is_pending_writer() - ): # until the current writer is finished - self.read_condition.wait(1) # wait on our condition - if self.queue and self.read_condition == self.queue[0]: - self.queue.pop(0) # then go ahead and remove it - self.readers += 1 # update the current number of readers - - def acquire_writer(self): - """Notify the lock that a new writer is requesting the underlying resource.""" - with self.lock: - if self.writer or self.readers: - condition = threading.Condition(self.lock) - # create a condition just for this writer - self.queue.append(condition) # and put it on the waiting queue - while self.writer or self.readers: # until the write lock is free - condition.wait(1) - self.queue.pop(0) - self.writer = True # stop other writers from operating - - def release_reader(self): - """Notify the lock that an existing reader is finished with the underlying resource.""" - with self.lock: - self.readers = max(0, self.readers - 1) # readers should never go below 0 - if not self.readers and self.queue: # if there are no active readers - self.queue[0].notify_all() # then notify any waiting writers - - def release_writer(self): - """Notify the lock that an existing writer is finished with the underlying resource.""" - with self.lock: - self.writer = False # give up current writing handle - if self.queue: # if someone is waiting in the queue - self.queue[0].notify_all() # wake them up first - else: - self.read_condition.notify_all() # otherwise wake up all possible readers - - @contextmanager - def get_reader_lock(self): - """Wrap some code with a reader lock using the python context manager protocol:: - - with rwlock.get_reader_lock(): - do_read_operation() - """ - try: - self.acquire_reader() - yield self - finally: - self.release_reader() - - @contextmanager - def get_writer_lock(self): - """Wrap some code with a writer lock using the python context manager protocol:: - - with rwlock.get_writer_lock(): - do_read_operation() - """ - try: - self.acquire_writer() - yield self - finally: - self.release_writer() - - -class ThreadSafeDataBlock(BaseModbusDataBlock): - """This is a simple decorator for a data block. - - This allows a user to inject an existing data block which can then be - safely operated on from multiple cocurrent threads. - - It should be noted that the choice was made to lock around the - datablock instead of the manager as there is less source of - contention (writes can occur to slave 0x01 while reads can - occur to slave 0x02). - """ - - def __init__(self, block): - """Initialize a new thread safe decorator - - :param block: The block to decorate - """ - self.rwlock = ReadWriteLock() - self.block = block - - def validate(self, address, count=1): - """Check to see if the request is in range - - :param address: The starting address - :param count: The number of values to test for - :returns: True if the request in within range, False otherwise - """ - with self.rwlock.get_reader_lock(): - return self.block.validate(address, count) - - def getValues(self, address, count=1): - """Return the requested values of the datastore - - :param address: The starting address - :param count: The number of values to retrieve - :returns: The requested values from a:a+c - """ - with self.rwlock.get_reader_lock(): - return self.block.getValues(address, count) - - def setValues(self, address, values): - """Set the requested values of the datastore - - :param address: The starting address - :param values: The new values to be set - """ - with self.rwlock.get_writer_lock(): - return self.block.setValues(address, values) - - -if __name__ == "__main__": # pylint: disable=too-complex - - class AtomicCounter: - """Atomic counter.""" - - def __init__(self, **kwargs): - """Init.""" - self.counter = kwargs.get("start", 0) - self.finish = kwargs.get("finish", 1000) - self.lock = threading.Lock() - - def increment(self, count=1): - """Increment.""" - with self.lock: - self.counter += count - - def is_running(self): - """Is running.""" - return self.counter <= self.finish - - locker = ReadWriteLock() - readers, writers = AtomicCounter(), AtomicCounter() - - def read(): - """Read.""" - while writers.is_running() and readers.is_running(): - with locker.get_reader_lock(): - readers.increment() - - def write(): - """Write.""" - while writers.is_running() and readers.is_running(): - with locker.get_writer_lock(): - writers.increment() - - rthreads = [threading.Thread(target=read) for i in range(50)] - wthreads = [threading.Thread(target=write) for i in range(2)] - for t in rthreads + wthreads: - t.start() - for t in rthreads + wthreads: - t.join() - print(f"readers[{readers.counter}] writers[{writers.counter}]") diff --git a/examples/v2.5.3/tornado_twisted/async_tornado_client.py b/examples/v2.5.3/tornado_twisted/async_tornado_client.py deleted file mode 100755 index 8d346f19b..000000000 --- a/examples/v2.5.3/tornado_twisted/async_tornado_client.py +++ /dev/null @@ -1,154 +0,0 @@ -#!/usr/bin/env python3 -"""Pymodbus Asynchronous Client Examples. - -The following is an example of how to use the asynchronous modbus -client implementation from pymodbus using Tornado. -""" -import functools -import logging - -from tornado.ioloop import IOLoop - -from pymodbus.client.asynchronous import schedulers - -# from pymodbus.client.asynchronous.udp import AsyncModbusUDPClient as ModbusClient -from pymodbus.client.asynchronous.tcp import ( - AsyncModbusTCPClient as ModbusClient, -) - - -# ---------------------------------------------------------------------------# -# choose the requested modbus protocol -# ---------------------------------------------------------------------------# - - -# ---------------------------------------------------------------------------# -# configure the client logging -# ---------------------------------------------------------------------------# -_logger = logging.getLogger() -_logger.setLevel(logging.DEBUG) - - -# ---------------------------------------------------------------------------# -# helper method to test deferred callbacks -# ---------------------------------------------------------------------------# - - -def dassert(future, callback): - """Dassert.""" - - def _assertor(value): - # by pass assertion, an error here stops the write callbacks - assert value - - def on_done(f_trans): - if exc := f_trans.exception(): - _logger.debug(exc) - return _assertor(False) - - return _assertor(callback(f_trans.result())) - - future.add_done_callback(on_done) - - -def _print(value): - """Print.""" - if hasattr(value, "bits"): - result = value.bits - elif hasattr(value, "registers"): - result = value.registers - else: - _logger.error(value) - return None - txt = f"Printing : -- {result}" - _logger.info(txt) - return result - - -UNIT = 0x01 - -# ---------------------------------------------------------------------------# -# example requests -# ---------------------------------------------------------------------------# -# simply call the methods that you would like to use. An example session -# is displayed below along with some assert checks. Note that unlike the -# synchronous version of the client, the asynchronous version returns -# deferreds which can be thought of as a handle to the callback to send -# the result of the operation. We are handling the result using the -# deferred assert helper(dassert). -# ---------------------------------------------------------------------------# - - -def begin_asynchronous_test(client, protocol): - """Begin async test.""" - rq = client.write_coil(1, True, unit=UNIT) - rr = client.read_coils(1, 1, unit=UNIT) - dassert(rq, lambda r: r.function_code < 0x80) # test for no error - dassert(rr, _print) # test the expected value - - rq = client.write_coils(1, [False] * 8, unit=UNIT) - rr = client.read_coils(1, 8, unit=UNIT) - dassert(rq, lambda r: r.function_code < 0x80) # test for no error - dassert(rr, _print) # test the expected value - - rq = client.write_coils(1, [False] * 8, unit=UNIT) - rr = client.read_discrete_inputs(1, 8, unit=UNIT) - dassert(rq, lambda r: r.function_code < 0x80) # test for no error - dassert(rr, _print) # test the expected value - - rq = client.write_register(1, 10, unit=UNIT) - rr = client.read_holding_registers(1, 1, unit=UNIT) - dassert(rq, lambda r: r.function_code < 0x80) # test for no error - dassert(rr, _print) # test the expected value - - rq = client.write_registers(1, [10] * 8, unit=UNIT) - rr = client.read_input_registers(1, 8, unit=UNIT) - dassert(rq, lambda r: r.function_code < 0x80) # test for no error - dassert(rr, _print) # test the expected value - - arguments = { - "read_address": 1, - "read_count": 8, - "write_address": 1, - "write_registers": [20] * 8, - } - rq = client.readwrite_registers(**arguments, unit=UNIT) - rr = client.read_input_registers(1, 8, unit=UNIT) - dassert(rq, lambda r: r.registers == [20] * 8) # test the expected value - dassert(rr, _print) # test the expected value - - # -----------------------------------------------------------------------# - # close the client at some time later - # -----------------------------------------------------------------------# - IOLoop.current().add_timeout(IOLoop.current().time() + 1, client.close) - IOLoop.current().add_timeout(IOLoop.current().time() + 2, protocol.stop) - - -# ---------------------------------------------------------------------------# -# choose the client you want -# ---------------------------------------------------------------------------# -# make sure to start an implementation to hit against. For this -# you can use an existing device, the reference implementation in the tools -# directory, or start a pymodbus server. -# ---------------------------------------------------------------------------# - - -def err(*args, **kwargs): - """Error.""" - txt = f"Err {args} {kwargs}" - _logger.error(txt) - - -def callback(protocol, future): - """Call as callback.""" - _logger.debug("Client connected") - if exp := future.exception(): - return err(exp) - - client = future.result() - return begin_asynchronous_test(client, protocol) - - -if __name__ == "__main__": - protocol, future = ModbusClient(schedulers.IO_LOOP, port=5020) - future.add_done_callback(functools.partial(callback, protocol)) diff --git a/examples/v2.5.3/tornado_twisted/async_tornado_client_serial.py b/examples/v2.5.3/tornado_twisted/async_tornado_client_serial.py deleted file mode 100755 index 18df10870..000000000 --- a/examples/v2.5.3/tornado_twisted/async_tornado_client_serial.py +++ /dev/null @@ -1,196 +0,0 @@ -#!/usr/bin/env python3 -"""Pymodbus Asynchronous Client Examples. - -The following is an example of how to use the asynchronous serial modbus -client implementation from pymodbus using tornado. -""" -import functools - -# ---------------------------------------------------------------------------# -# import needed libraries -# ---------------------------------------------------------------------------# -import logging -from tempfile import gettempdir - -from tornado.ioloop import IOLoop - -from pymodbus.client.asynchronous import schedulers -from pymodbus.client.asynchronous.serial import AsyncModbusSerialClient - - -# ---------------------------------------------------------------------------# -# choose the requested modbus protocol -# ---------------------------------------------------------------------------# - - -# ---------------------------------------------------------------------------# -# configure the client logging -# ---------------------------------------------------------------------------# - -FORMAT = ( - "%(asctime)-15s %(threadName)-15s" - " %(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s" -) -log = logging.getLogger() -log.setLevel(logging.DEBUG) - -UNIT = 0x01 - -# ---------------------------------------------------------------------------# -# helper method to test deferred callbacks -# ---------------------------------------------------------------------------# - - -def dassert(future, callback): - """Dassert.""" - - def _assertor(value): - # by pass assertion, an error here stops the write callbacks - assert value - - def on_done(f_trans): - if exc := f_trans.exception(): - log.debug(exc) - return _assertor(False) - - return _assertor(callback(f_trans.result())) - - future.add_done_callback(on_done) - - -def _print(value): - """Print.""" - if hasattr(value, "bits"): - result = value.bits - elif hasattr(value, "registers"): - result = value.registers - else: - log.error(value) - return None - txt = f"Printing : -- {result}" - log.info(txt) - return result - - -# ---------------------------------------------------------------------------# -# example requests -# ---------------------------------------------------------------------------# -# simply call the methods that you would like to use. An example session -# is displayed below along with some assert checks. Note that unlike the -# synchronous version of the client, the asynchronous version returns -# deferreds which can be thought of as a handle to the callback to send -# the result of the operation. We are handling the result using the -# deferred assert helper(dassert). -# ---------------------------------------------------------------------------# - - -def begin_asynchronous_test(client, protocol): - """Begin async test.""" - rq = client.write_coil(1, True, unit=UNIT) - rr = client.read_coils(1, 1, unit=UNIT) - dassert(rq, lambda r: r.function_code < 0x80) # test for no error - dassert(rr, _print) # test the expected value - - rq = client.write_coils(1, [False] * 8, unit=UNIT) - rr = client.read_coils(1, 8, unit=UNIT) - dassert(rq, lambda r: r.function_code < 0x80) # test for no error - dassert(rr, _print) # test the expected value - - rq = client.write_coils(1, [False] * 8, unit=UNIT) - rr = client.read_discrete_inputs(1, 8, unit=UNIT) - dassert(rq, lambda r: r.function_code < 0x80) # test for no error - dassert(rr, _print) # test the expected value - - rq = client.write_register(1, 10, unit=UNIT) - rr = client.read_holding_registers(1, 1, unit=UNIT) - dassert(rq, lambda r: r.function_code < 0x80) # test for no error - dassert(rr, _print) # test the expected value - - rq = client.write_registers(1, [10] * 8, unit=UNIT) - rr = client.read_input_registers(1, 8, unit=UNIT) - dassert(rq, lambda r: r.function_code < 0x80) # test for no error - dassert(rr, _print) # test the expected value - - arguments = { - "read_address": 1, - "read_count": 8, - "write_address": 1, - "write_registers": [20] * 8, - } - rq = client.readwrite_registers(**arguments, unit=UNIT) - rr = client.read_input_registers(1, 8, unit=UNIT) - dassert(rq, lambda r: r.registers == [20] * 8) # test the expected value - dassert(rr, _print) # test the expected value - - # -----------------------------------------------------------------------# - # close the client at some time later - # -----------------------------------------------------------------------# - IOLoop.current().add_timeout(IOLoop.current().time() + 1, client.close) - IOLoop.current().add_timeout(IOLoop.current().time() + 2, protocol.stop) - - -# ---------------------------------------------------------------------------# -# choose the client you want -# ---------------------------------------------------------------------------# -# make sure to start an implementation to hit against. For this -# you can use an existing device, the reference implementation in the tools -# directory, or start a pymodbus server. -# ---------------------------------------------------------------------------# - - -def err(*args, **kwargs): - """Handle error.""" - txt = f"Err {args} {kwargs}" - log.error(txt) - - -def callback(protocol, future): - """Call Callback.""" - log.debug("Client connected") - if exp := future.exception(): - return err(exp) - - client = future.result() - return begin_asynchronous_test(client, protocol) - - -if __name__ == "__main__": - # ----------------------------------------------------------------------- # - # Create temporary serial ports using SOCAT - - # socat -d -d PTY,link=/tmp/ptyp0,raw,echo=0,ispeed=9600 PTY, - # link=/tmp/ttyp0,raw,echo=0,ospeed=9600 - - # Default framer is ModbusRtuFramer - # ----------------------------------------------------------------------- # - - # Rtu - ( - protocol, - future, - ) = AsyncModbusSerialClient( # pylint: disable=unpacking-non-sequence - schedulers.IO_LOOP, - method="rtu", - port=gettempdir() + "/ptyp0", - baudrate=9600, - timeout=2, - ) - - # Asci - # from pymodbus.transaction import ModbusAsciiFramer - # protocol, future = AsyncModbusSerialClient(schedulers.IO_LOOP, - # method="ascii", - # port="/dev/ptyp0", - # framer=ModbusAsciiFramer, - # baudrate=9600, - # timeout=2) - - # Binary - # from pymodbus.transaction import ModbusBinaryFramer - # protocol, future = AsyncModbusSerialClient(schedulers.IO_LOOP, - # method="binary", - # port="/dev/ptyp0", - # framer=ModbusBinaryFramer, - # baudrate=9600, - # timeout=2) - future.add_done_callback(functools.partial(callback, protocol)) diff --git a/examples/v2.5.3/tornado_twisted/async_twisted_client.py b/examples/v2.5.3/tornado_twisted/async_twisted_client.py deleted file mode 100755 index 3aa7a15c0..000000000 --- a/examples/v2.5.3/tornado_twisted/async_twisted_client.py +++ /dev/null @@ -1,176 +0,0 @@ -#!/usr/bin/env python3 -"""Pymodbus Asynchronous Client Examples. - -The following is an example of how to use the asynchronous modbus -client implementation from pymodbus. -""" -# --------------------------------------------------------------------------- # -# import needed libraries -# --------------------------------------------------------------------------- # -import logging - -from twisted.internet import reactor - -# from pymodbus.client.asynchronous.udp import AsyncModbusUDPClient -from pymodbus.client.asynchronous import schedulers -from pymodbus.client.asynchronous.tcp import AsyncModbusTCPClient - - -# --------------------------------------------------------------------------- # -# configure the client logging -# --------------------------------------------------------------------------- # -FORMAT = ( - "%(asctime)-15s %(threadName)-15s" - " %(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s" -) -logging.basicConfig(format=FORMAT) -log = logging.getLogger() -log.setLevel(logging.DEBUG) - -# --------------------------------------------------------------------------- # -# helper method to test deferred callbacks -# --------------------------------------------------------------------------- # - - -def err(*args, **kwargs): - """Error.""" - txt = f"Err-{args}-{kwargs}" - logging.error(txt) - - -def dassert(deferred, callback): - """Dassert.""" - - def _assertor(value): - assert value - - deferred.addCallback(lambda r: _assertor(callback(r))) - deferred.addErrback(err) - - -# --------------------------------------------------------------------------- # -# specify slave to query -# --------------------------------------------------------------------------- # -# The slave to query is specified in an optional parameter for each -# individual request. This can be done by specifying the `unit` parameter -# which defaults to `0x00` -# --------------------------------------------------------------------------- # - - -UNIT = 0x01 - - -def process_response(result): - """Process response.""" - log.debug(result) - - -def example_requests(client): - """Do example requests.""" - rr = client.read_coils(1, 1, unit=0x02) - rr.addCallback(process_response) - rr = client.read_holding_registers(1, 1, unit=0x02) - rr.addCallback(process_response) - rr = client.read_discrete_inputs(1, 1, unit=0x02) - rr.addCallback(process_response) - rr = client.read_input_registers(1, 1, unit=0x02) - rr.addCallback(process_response) - stop_asynchronous_test(client) - - -# --------------------------------------------------------------------------- # -# example requests -# --------------------------------------------------------------------------- # -# simply call the methods that you would like to use. An example session -# is displayed below along with some assert checks. Note that unlike the -# synchronous version of the client, the asynchronous version returns -# deferreds which can be thought of as a handle to the callback to send -# the result of the operation. We are handling the result using the -# deferred assert helper(dassert). -# --------------------------------------------------------------------------- # - - -def stop_asynchronous_test(client): - """Stop async test.""" - # ----------------------------------------------------------------------- # - # close the client at some time later - # ----------------------------------------------------------------------- # - reactor.callLater(1, client.transport.loseConnection) - reactor.callLater(2, reactor.stop) - - -def begin_asynchronous_test(client): - """Begin async test.""" - rq = client.write_coil(1, True, unit=UNIT) - rr = client.read_coils(1, 1, unit=UNIT) - dassert(rq, lambda r: not r.isError()) # test for no error - dassert(rr, lambda r: r.bits[0]) # test the expected value - - rq = client.write_coils(1, [True] * 8, unit=UNIT) - rr = client.read_coils(1, 8, unit=UNIT) - dassert(rq, lambda r: not r.isError()) # test for no error - dassert(rr, lambda r: r.bits == [True] * 8) # test the expected value - - rq = client.write_coils(1, [False] * 8, unit=UNIT) - rr = client.read_discrete_inputs(1, 8, unit=UNIT) - dassert(rq, lambda r: not r.isError()) # test for no error - dassert(rr, lambda r: r.bits == [True] * 8) # test the expected value - - rq = client.write_register(1, 10, unit=UNIT) - rr = client.read_holding_registers(1, 1, unit=UNIT) - dassert(rq, lambda r: not r.isError()) # test for no error - dassert(rr, lambda r: r.registers[0] == 10) # test the expected value - - rq = client.write_registers(1, [10] * 8, unit=UNIT) - rr = client.read_input_registers(1, 8, unit=UNIT) - dassert(rq, lambda r: not r.isError()) # test for no error - dassert(rr, lambda r: r.registers == [17] * 8) # test the expected value - - arguments = { - "read_address": 1, - "read_count": 8, - "write_address": 1, - "write_registers": [20] * 8, - } - rq = client.readwrite_registers(arguments, unit=UNIT) - rr = client.read_input_registers(1, 8, unit=UNIT) - dassert(rq, lambda r: r.registers == [20] * 8) # test the expected value - dassert(rr, lambda r: r.registers == [17] * 8) # test the expected value - stop_asynchronous_test(client) - - # ----------------------------------------------------------------------- # - # close the client at some time later - # ----------------------------------------------------------------------- # - # reactor.callLater(1, client.transport.loseConnection) - reactor.callLater(2, reactor.stop) - - -# --------------------------------------------------------------------------- # -# extra requests -# --------------------------------------------------------------------------- # -# If you are performing a request that is not available in the client -# mixin, you have to perform the request like this instead:: -# -# from pymodbus.diag_message import ClearCountersRequest -# from pymodbus.diag_message import ClearCountersResponse -# -# request = ClearCountersRequest() -# response = client.execute(request) -# if isinstance(response, ClearCountersResponse): -# ... do something with the response -# -# --------------------------------------------------------------------------- # - -# --------------------------------------------------------------------------- # -# choose the client you want -# --------------------------------------------------------------------------- # -# make sure to start an implementation to hit against. For this -# you can use an existing device, the reference implementation in the tools -# directory, or start a pymodbus server. -# --------------------------------------------------------------------------- # - - -if __name__ == "__main__": - protocol, deferred = AsyncModbusTCPClient(schedulers.REACTOR, port=5020) - deferred.addCallback(begin_asynchronous_test) - deferred.addErrback(err) diff --git a/examples/v2.5.3/tornado_twisted/async_twisted_client_serial.py b/examples/v2.5.3/tornado_twisted/async_twisted_client_serial.py deleted file mode 100755 index f7062b000..000000000 --- a/examples/v2.5.3/tornado_twisted/async_twisted_client_serial.py +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env python3 -"""Pymodbus Asynchronous Client Examples. - -The following is an example of how to use the asynchronous serial modbus -client implementation from pymodbus with twisted. -""" -import logging -from tempfile import gettempdir - -from twisted.internet import reactor - -from pymodbus.client.asynchronous import schedulers -from pymodbus.client.asynchronous.serial import AsyncModbusSerialClient -from pymodbus.client.asynchronous.twisted import ModbusClientProtocol - - -log = logging.getLogger("pymodbus") -log.setLevel(logging.DEBUG) - -# ---------------------------------------------------------------------------# -# state a few constants -# ---------------------------------------------------------------------------# - -SERIAL_PORT = gettempdir() + "/ptyp0" -STATUS_REGS = (1, 2) -STATUS_COILS = (1, 3) -CLIENT_DELAY = 1 -UNIT = 0x01 - - -class ExampleProtocol(ModbusClientProtocol): - """Example protocol.""" - - def __init__(self, framer): - """Initialize our custom protocol - - :param framer: The decoder to use to process messages - :param endpoint: The endpoint to send results to - """ - ModbusClientProtocol.__init__(self, framer) - log.debug("Beginning the processing loop") - reactor.callLater(CLIENT_DELAY, self.fetch_holding_registers) - - def fetch_holding_registers(self): - """Defer fetching holding registers""" - log.debug("Starting the next cycle") - result = self.read_holding_registers(*STATUS_REGS, unit=UNIT) - result.addCallbacks(self.send_holding_registers, self.error_handler) - - def send_holding_registers(self, response): - """Write values of holding registers, defer fetching coils - - :param response: The response to process - """ - log.info(response.getRegister(0)) - log.info(response.getRegister(1)) - result = self.read_coils(*STATUS_COILS, unit=UNIT) - result.addCallbacks(self.start_next_cycle, self.error_handler) - - def start_next_cycle(self, response): - """Write values of coils, trigger next cycle - - :param response: The response to process - """ - log.info(response.getBit(0)) - log.info(response.getBit(1)) - log.info(response.getBit(2)) - reactor.callLater(CLIENT_DELAY, self.fetch_holding_registers) - - def error_handler(self, failure): - """Handle any twisted errors - - :param failure: The error to handle - """ - log.error(failure) - - -if __name__ == "__main__": - import time - - proto, client = AsyncModbusSerialClient( - schedulers.REACTOR, - method="rtu", - port=SERIAL_PORT, - timeout=2, - proto_cls=ExampleProtocol, - ) - proto.start() - time.sleep(10) # Wait for operation to complete - # proto.stop() diff --git a/examples/v2.5.3/tornado_twisted/asynchronous_processor.py b/examples/v2.5.3/tornado_twisted/asynchronous_processor.py deleted file mode 100755 index 2e2fd3bfa..000000000 --- a/examples/v2.5.3/tornado_twisted/asynchronous_processor.py +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env python3 -"""Pymodbus Asynchronous Processor Example. - -The following is a full example of a continuous client processor. Feel -free to use it as a skeleton guide in implementing your own. -""" -# --------------------------------------------------------------------------- # -# import the necessary modules -# --------------------------------------------------------------------------- # -import logging - -from twisted.internet import reactor, serialport -from twisted.internet.protocol import ClientFactory - -from pymodbus.client.asynchronous.twisted import ModbusClientProtocol -from pymodbus.factory import ClientDecoder - -# --------------------------------------------------------------------------- # -# Choose the framer you want to use -# --------------------------------------------------------------------------- # -# from pymodbus.transaction import ModbusBinaryFramer as ModbusFramer -# from pymodbus.transaction import ModbusAsciiFramer as ModbusFramer -from pymodbus.transaction import ModbusRtuFramer as ModbusFramer - - -# from pymodbus.transaction import ModbusSocketFramer as ModbusFramer - -# --------------------------------------------------------------------------- # -# configure the client logging -# --------------------------------------------------------------------------- # -FORMAT = ( - "%(asctime)-15s %(threadName)-15s" - " %(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s" -) -logging.basicConfig(format=FORMAT) -log = logging.getLogger() -log.setLevel(logging.DEBUG) - -# --------------------------------------------------------------------------- # -# state a few constants -# --------------------------------------------------------------------------- # - -SERIAL_PORT = "/dev/ptyp0" -STATUS_REGS = (1, 2) -STATUS_COILS = (1, 3) -CLIENT_DELAY = 1 -UNIT = 0x01 - - -# --------------------------------------------------------------------------- # -# an example custom protocol -# --------------------------------------------------------------------------- # -# Here you can perform your main processing loop utilizing defereds and timed -# callbacks. -# --------------------------------------------------------------------------- # -class ExampleProtocol(ModbusClientProtocol): - """Example protocol.""" - - def __init__(self, framer, endpoint): - """Initialize our custom protocol - - :param framer: The decoder to use to process messages - :param endpoint: The endpoint to send results to - """ - ModbusClientProtocol.__init__(self, framer) - self.endpoint = endpoint - log.debug("Beginning the processing loop") - reactor.callLater(CLIENT_DELAY, self.fetch_holding_registers) - - def fetch_holding_registers(self): - """Defer fetching holding registers""" - log.debug("Starting the next cycle") - data = self.read_holding_registers(*STATUS_REGS, unit=UNIT) - data.addCallbacks(self.send_holding_registers, self.error_handler) - - def send_holding_registers(self, response): - """Write values of holding registers, defer fetching coils - - :param response: The response to process - """ - self.endpoint.write(response.getRegister(0)) - self.endpoint.write(response.getRegister(1)) - result = self.read_coils(*STATUS_COILS, unit=UNIT) - result.addCallbacks(self.start_next_cycle, self.error_handler) - - def start_next_cycle(self, response): - """Write values of coils, trigger next cycle - - :param response: The response to process - """ - self.endpoint.write(response.getBit(0)) - self.endpoint.write(response.getBit(1)) - self.endpoint.write(response.getBit(2)) - reactor.callLater(CLIENT_DELAY, self.fetch_holding_registers) - - def error_handler(self, failure): - """Handle any twisted errors - - :param failure: The error to handle - """ - log.error(failure) - - -# --------------------------------------------------------------------------- # -# a factory for the example protocol -# --------------------------------------------------------------------------- # -# This is used to build client protocol's if you tie into twisted's method -# of processing. It basically produces client instances of the underlying -# protocol:: -# -# Factory(Protocol) -> ProtocolInstance -# -# It also persists data between client instances (think protocol singleton). -# --------------------------------------------------------------------------- # -class ExampleFactory(ClientFactory): - """Example factory.""" - - protocol = ExampleProtocol - - def __init__(self, framer, endpoint): - """Remember things necessary for building a protocols""" - self.framer = framer - self.endpoint = endpoint - - def buildProtocol(self, _): - """Create a protocol and start the reading cycle""" - proto = self.protocol(self.framer, self.endpoint) - proto.factory = self - return proto - - -# --------------------------------------------------------------------------- # -# a custom client for our device -# --------------------------------------------------------------------------- # -# Twisted provides a number of helper methods for creating and starting -# clients: -# - protocol.ClientCreator -# - reactor.connectTCP -# -# How you start your client is really up to you. -# --------------------------------------------------------------------------- # -class SerialModbusClient(serialport.SerialPort): - """Serial modbus client.""" - - def __init__(self, factory, *args, **kwargs): - """Do setup the client and start listening on the serial port - - :param factory: The factory to build clients with - """ - protocol = factory.buildProtocol(None) - self.decoder = ClientDecoder() - serialport.SerialPort.__init__(self, protocol, *args, **kwargs) - - -# --------------------------------------------------------------------------- # -# a custom endpoint for our results -# --------------------------------------------------------------------------- # -# An example line reader, this can replace with: -# - the TCP protocol -# - a context recorder -# - a database or file recorder -# --------------------------------------------------------------------------- # -class LoggingLineReader: - """Logging line reader.""" - - def write(self, response): - """Handle the next modbus response - - :param response: The response to process - """ - txt = f"Read Data: {response}" - log.info(txt) - - -# --------------------------------------------------------------------------- # -# start running the processor -# --------------------------------------------------------------------------- # -# This initializes the client, the framer, the factory, and starts the -# twisted event loop (the reactor). It should be noted that a number of -# things could be chanegd as one sees fit: -# - The ModbusRtuFramer could be replaced with a ModbusAsciiFramer -# - The SerialModbusClient could be replaced with reactor.connectTCP -# - The LineReader endpoint could be replaced with a database store -# --------------------------------------------------------------------------- # - - -def main(): - """Run Main.""" - log.debug("Initializing the client") - framer = ModbusFramer(ClientDecoder(), client=None) - reader = LoggingLineReader() - factory = ExampleFactory(framer, reader) - SerialModbusClient(factory, SERIAL_PORT, reactor) - # factory = reactor.connectTCP("localhost", 502, factory) - log.debug("Starting the client") - reactor.run() - - -if __name__ == "__main__": - main() diff --git a/examples/v2.5.3/tornado_twisted/asynchronous_server.py b/examples/v2.5.3/tornado_twisted/asynchronous_server.py deleted file mode 100755 index 780e4214f..000000000 --- a/examples/v2.5.3/tornado_twisted/asynchronous_server.py +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env python3 -"""Pymodbus Asynchronous Server Example. - -The asynchronous server is a high performance implementation using the -twisted library as its backend. This allows it to scale to many thousands -of nodes which can be helpful for testing monitoring software. -""" -# --------------------------------------------------------------------------- # -# import the various server implementations -# --------------------------------------------------------------------------- # -import logging - -from custom_message import CustomModbusRequest - -from pymodbus import __version__ as pymodbus_version -from pymodbus.datastore import ( - ModbusSequentialDataBlock, - ModbusServerContext, - ModbusSlaveContext, -) - -# from pymodbus.server.asynchronous import StartUdpServer -# from pymodbus.server.asynchronous import StartSerialServer -from pymodbus.device import ModbusDeviceIdentification -from pymodbus.server import StartTcpServer - - -# from pymodbus.transaction import ( -# ModbusRtuFramer, -# ModbusAsciiFramer, -# ModbusBinaryFramer, -# ) - -# --------------------------------------------------------------------------- # -# configure the service logging -# --------------------------------------------------------------------------- # -FORMAT = ( - "%(asctime)-15s %(threadName)-15s" - " %(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s" -) -logging.basicConfig(format=FORMAT) -log = logging.getLogger() -log.setLevel(logging.DEBUG) - - -def run_async_server(): - """Run async server.""" - # ----------------------------------------------------------------------- # - # initialize your data store - # ----------------------------------------------------------------------- # - # The datastores only respond to the addresses that they are initialized to - # Therefore, if you initialize a DataBlock to addresses from 0x00 to 0xFF, - # a request to 0x100 will respond with an invalid address exception. - # This is because many devices exhibit this kind of behavior (but not all) - # - # block = ModbusSequentialDataBlock(0x00, [0]*0xff) - # - # Continuing, you can choose to use a sequential or a sparse DataBlock in - # your data context. The difference is that the sequential has no gaps in - # the data while the sparse can. Once again, there are devices that exhibit - # both forms of behavior:: - # - # block = ModbusSparseDataBlock({0x00: 0, 0x05: 1}) - # block = ModbusSequentialDataBlock(0x00, [0]*5) - # - # Alternately, you can use the factory methods to initialize the DataBlocks - # or simply do not pass them to have them initialized to 0x00 on the full - # address range:: - # - # store = ModbusSlaveContext(di = ModbusSequentialDataBlock.create()) - # store = ModbusSlaveContext() - # - # Finally, you are allowed to use the same DataBlock reference for every - # table or you you may use a separate DataBlock for each table. - # This depends if you would like functions to be able to access and modify - # the same data or not:: - # - # block = ModbusSequentialDataBlock(0x00, [0]*0xff) - # store = ModbusSlaveContext(di=block, co=block, hr=block, ir=block) - # - # The server then makes use of a server context that allows the server to - # respond with different slave contexts for different unit ids. By default - # it will return the same context for every unit id supplied (broadcast - # mode). - # However, this can be overloaded by setting the single flag to False - # and then supplying a dictionary of unit id to context mapping:: - # - # slaves = { - # 0x01: ModbusSlaveContext(...), - # 0x02: ModbusSlaveContext(...), - # 0x03: ModbusSlaveContext(...), - # } - # context = ModbusServerContext(slaves=slaves, single=False) - # - # The slave context can also be initialized in zero_mode which means that a - # request to address(0-7) will map to the address (0-7). The default is - # False which is based on section 4.4 of the specification, so address(0-7) - # will map to (1-8):: - # - # store = ModbusSlaveContext(..., zero_mode=True) - # ----------------------------------------------------------------------- # - store = ModbusSlaveContext( - co=ModbusSequentialDataBlock(0, [17] * 100), - di=ModbusSequentialDataBlock(0, [17] * 100), - hr=ModbusSequentialDataBlock(0, [17] * 100), - ir=ModbusSequentialDataBlock(0, [17] * 100), - ) - store.register( - CustomModbusRequest.function_code, - "cm", - ModbusSequentialDataBlock(0, [17] * 100), - ) - context = ModbusServerContext(slaves=store, single=True) - - # ----------------------------------------------------------------------- # - # initialize the server information - # ----------------------------------------------------------------------- # - # If you don't set this or any fields, they are defaulted to empty strings. - # ----------------------------------------------------------------------- # - identity = ModbusDeviceIdentification( - info_name={ - "VendorName": "Pymodbus", - "ModelName": "Pymodbus Server", - "MajorMinorRevision": pymodbus_version, - "ProductCode": "PM", - "VendorUrl": "https://github.com/pymodbus-dev/pymodbus/", - "ProductName": "Pymodbus Server", - } - ) - - # ----------------------------------------------------------------------- # - # run the server you want - # ----------------------------------------------------------------------- # - - # TCP Server - - StartTcpServer( - context, - identity=identity, - address=("localhost", 5020), - custom_functions=[CustomModbusRequest], - ) - - # TCP Server with deferred reactor run - - # from twisted.internet import reactor - # StartTcpServer(context, identity=identity, address=("localhost", 5020), - # defer_reactor_run=True) - # reactor.run() - - # Server with RTU framer - # StartTcpServer(context, identity=identity, address=("localhost", 5020), - # framer=ModbusRtuFramer) - - # UDP Server - # StartUdpServer(context, identity=identity, address=("127.0.0.1", 5020)) - - # RTU Server - # StartSerialServer(context, identity=identity, - # port="/dev/ttyp0", framer=ModbusRtuFramer) - - # ASCII Server - # StartSerialServer(context, identity=identity, - # port="/dev/ttyp0", framer=ModbusAsciiFramer) - - # Binary Server - # StartSerialServer(context, identity=identity, - # port="/dev/ttyp0", framer=ModbusBinaryFramer) - - -if __name__ == "__main__": - run_async_server() diff --git a/examples/v2.5.3/tornado_twisted/modbus_scraper.py b/examples/v2.5.3/tornado_twisted/modbus_scraper.py deleted file mode 100755 index 7c59a1754..000000000 --- a/examples/v2.5.3/tornado_twisted/modbus_scraper.py +++ /dev/null @@ -1,320 +0,0 @@ -#!/usr/bin/env python3 -"""This is a simple scraper. - -that can be pointed at a modbus device to pull down all its values and store -them as a collection of sequential data blocks. -""" -import logging -import pickle -from optparse import OptionParser - -from twisted.internet import ( # pylint: disable=import-error - reactor, - serialport, -) -from twisted.internet.protocol import ( - ClientFactory, # pylint: disable=import-error -) - -from pymodbus.client.asynchronous.twisted import ModbusClientProtocol -from pymodbus.datastore import ModbusSequentialDataBlock, ModbusSlaveContext -from pymodbus.factory import ClientDecoder - -# --------------------------------------------------------------------------- # -# Choose the framer you want to use -# --------------------------------------------------------------------------- # -# from pymodbus.transaction import ModbusBinaryFramer -# from pymodbus.transaction import ModbusAsciiFramer -# from pymodbus.transaction import ModbusRtuFramer -from pymodbus.transaction import ModbusSocketFramer - - -# -------------------------------------------------------------------------- # -# Configure the client logging -# -------------------------------------------------------------------------- # -log = logging.getLogger("pymodbus") - - -# --------------------------------------------------------------------------- # -# Define some constants -# --------------------------------------------------------------------------- # -COUNT = 8 # The number of bits/registers to read at once -DELAY = 0 # The delay between subsequent reads -SLAVE = 0x01 # The slave unit id to read from - -# --------------------------------------------------------------------------- # -# A simple scraper protocol -# --------------------------------------------------------------------------- # -# I tried to spread the load across the device, but feel free to modify the -# logic to suit your own purpose. -# --------------------------------------------------------------------------- # - - -class ScraperProtocol(ModbusClientProtocol): - """Scraper protocol.""" - - address = None - - def __init__(self, framer, endpoint): - """Initialize our custom protocol - - :param framer: The decoder to use to process messages - :param endpoint: The endpoint to send results to - """ - ModbusClientProtocol.__init__(self, framer) - self.endpoint = endpoint - - def connection_made(self): - """Call when the client has connected to the remote server.""" - super().connectionMade() - log.debug("Beginning the processing loop") - self.address = self.factory.starting - reactor.callLater(DELAY, self.scrape_holding_registers) - - def connection_lost(self, reason): - """Call when the client disconnects from the server. - - :param reason: The reason for the disconnection - """ - reactor.callLater(DELAY, reactor.stop) - - def scrape_holding_registers(self): - """Defer fetching holding registers""" - txt = f"reading holding registers: {self.address}" - log.debug(txt) - data = self.read_holding_registers(self.address, count=COUNT, unit=SLAVE) - data.addCallbacks(self.scrape_discrete_inputs, self.error_handler) - - def scrape_discrete_inputs(self, response): - """Defer fetching holding registers""" - txt = f"reading discrete inputs: {self.address}" - log.debug(txt) - self.endpoint.write((3, self.address, response.registers)) - data = self.read_discrete_inputs(self.address, count=COUNT, unit=SLAVE) - data.addCallbacks(self.scrape_input_registers, self.error_handler) - - def scrape_input_registers(self, response): - """Defer fetching holding registers""" - txt = f"reading discrete inputs: {self.address}" - log.debug(txt) - self.endpoint.write((2, self.address, response.bits)) - data = self.read_input_registers(self.address, count=COUNT, unit=SLAVE) - data.addCallbacks(self.scrape_coils, self.error_handler) - - def scrape_coils(self, response): - """Write values of holding registers, defer fetching coils - - :param response: The response to process - """ - txt = f"reading coils: {self.address}" - log.debug(txt) - self.endpoint.write((4, self.address, response.registers)) - data = self.read_coils(self.address, count=COUNT, unit=SLAVE) - data.addCallbacks(self.start_next_cycle, self.error_handler) - - def start_next_cycle(self, response): - """Write values of coils, trigger next cycle - - :param response: The response to process - """ - txt = f"starting next round: {self.address}" - log.debug(txt) - self.endpoint.write((1, self.address, response.bits)) - self.address += COUNT - if self.address >= self.factory.ending: - self.endpoint.finalize() - self.transport.loseConnection() - else: - reactor.callLater(DELAY, self.scrape_holding_registers) - - def error_handler(self, failure): - """Handle any twisted errors - - :param failure: The error to handle - """ - log.error(failure) - - -# --------------------------------------------------------------------------- # -# a factory for the example protocol -# --------------------------------------------------------------------------- # -# This is used to build client protocol's if you tie into twisted's method -# of processing. It basically produces client instances of the underlying -# protocol:: -# -# Factory(Protocol) -> ProtocolInstance -# -# It also persists data between client instances (think protocol singleton). -# --------------------------------------------------------------------------- # -class ScraperFactory(ClientFactory): # pylint: disable=too-few-public-methods - """Scraper factory.""" - - protocol = ScraperProtocol - - def __init__(self, framer, endpoint, query): - """Remember things necessary for building a protocols""" - self.framer = framer - self.endpoint = endpoint - self.starting, self.ending = query - - def buildProtocol(self, _): # pylint: disable=invalid-name - """Create a protocol and start the reading cycle""" - protocol = self.protocol(self.framer, self.endpoint) - protocol.factory = self # pylint: disable=attribute-defined-outside-init - return protocol - - -# --------------------------------------------------------------------------- # -# a custom client for our device -# --------------------------------------------------------------------------- # -# Twisted provides a number of helper methods for creating and starting -# clients: -# - protocol.ClientCreator -# - reactor.connectTCP -# -# How you start your client is really up to you. -# --------------------------------------------------------------------------- # -class SerialModbusClient(serialport.SerialPort): - """Serial modbus client.""" - - def __init__(self, factory, *args, **kwargs): - """Do setup the client and start listening on the serial port - - :param factory: The factory to build clients with - """ - protocol = factory.buildProtocol(None) - self.decoder = ClientDecoder() - serialport.SerialPort.__init__(self, protocol, *args, **kwargs) - - -# --------------------------------------------------------------------------- # -# a custom endpoint for our results -# --------------------------------------------------------------------------- # -# An example line reader, this can replace with: -# - the TCP protocol -# - a context recorder -# - a database or file recorder -# --------------------------------------------------------------------------- # -class LoggingContextReader: - """Logging context reader.""" - - def __init__(self, output): - """Initialize a new instance of the logger - - :param output: The output file to save to - """ - self.output = output - self.context = ModbusSlaveContext( - di=ModbusSequentialDataBlock.create(), - co=ModbusSequentialDataBlock.create(), - hr=ModbusSequentialDataBlock.create(), - ir=ModbusSequentialDataBlock.create(), - ) - - def write(self, response): - """Handle the next modbus response - - :param response: The response to process - """ - txt = f"Read Data: {str(response)}" - log.info(txt) - file_ptr, address, values = response - self.context.setValues(file_ptr, address, values) - - def finalize(self): - """Finalize.""" - with open(self.output, "w") as handle: - pickle.dump(self.context, handle) - - -# -------------------------------------------------------------------------- # -# Main start point -# -------------------------------------------------------------------------- # -def get_options(): - """Parse the command line options - - :returns: The options manager - """ - parser = OptionParser() - - parser.add_option( - "-o", - "--output", - help="The resulting output file for the scrape", - dest="output", - default="datastore.pickle", - ) - - parser.add_option( - "-p", - "--port", - help="The port to connect to", - type="int", - dest="port", - default=502, - ) - - parser.add_option( - "-s", "--server", help="The server to scrape", dest="host", default="127.0.0.1" - ) - - parser.add_option( - "-r", - "--range", - help="The address range to scan", - dest="query", - default="0:1000", - ) - - parser.add_option( - "-d", - "--debug", - help="Enable debug tracing", - action="store_true", - dest="debug", - default=False, - ) - - (opt, _) = parser.parse_args() - return opt - - -def main(): - """Run main runner function""" - options = get_options() - - if options.debug: - try: - log.setLevel(logging.DEBUG) - except Exception: - print("Logging is not supported on this system") - - # split the query into a starting and ending range - query = [int(p) for p in options.query.split(":")] - - try: - log.debug("Initializing the client") - framer = ModbusSocketFramer(ClientDecoder()) - reader = LoggingContextReader(options.output) - factory = ScraperFactory(framer, reader, query) - - # how to connect based on TCP vs Serial clients - if isinstance(framer, ModbusSocketFramer): - reactor.connectTCP(options.host, options.port, factory) - else: - SerialModbusClient(factory, options.port, reactor) - - log.debug("Starting the client") - reactor.run() - log.debug("Finished scraping the client") - except Exception as exc: - print(exc) - - -# --------------------------------------------------------------------------- # -# Main jumper -# --------------------------------------------------------------------------- # - - -if __name__ == "__main__": - main() diff --git a/examples/v2.5.3/tornado_twisted/sunspec_client.py b/examples/v2.5.3/tornado_twisted/sunspec_client.py deleted file mode 100644 index ccada694d..000000000 --- a/examples/v2.5.3/tornado_twisted/sunspec_client.py +++ /dev/null @@ -1,309 +0,0 @@ -"""Sunspec client.""" -import logging - -from twisted.internet.defer import Deferred # pylint: disable=import-error - -from pymodbus.client.sync import ModbusTcpClient -from pymodbus.constants import Endian -from pymodbus.payload import BinaryPayloadDecoder - - -# --------------------------------------------------------------------------- # -# Logging -# --------------------------------------------------------------------------- # -_logger = logging.getLogger(__name__) -_logger.setLevel(logging.DEBUG) - - -# --------------------------------------------------------------------------- # -# Sunspec Common Constants -# --------------------------------------------------------------------------- # -class SunspecDefaultValue: - """A collection of constants to indicate if a value is not implemented.""" - - Signed16 = 0x8000 - Unsigned16 = 0xFFFF - Accumulator16 = 0x0000 - Scale = 0x8000 - Signed32 = 0x80000000 - Float32 = 0x7FC00000 - Unsigned32 = 0xFFFFFFFF - Accumulator32 = 0x00000000 - Signed64 = 0x8000000000000000 - Unsigned64 = 0xFFFFFFFFFFFFFFFF - Accumulator64 = 0x0000000000000000 - String = "\x00" - - -class SunspecStatus: - """Indicators of the current status of a sunspec device""" - - Normal = 0x00000000 - Error = 0xFFFFFFFE - Unknown = 0xFFFFFFFF - - -class SunspecIdentifier: - """Assigned identifiers that are pre-assigned by the sunspec protocol.""" - - Sunspec = 0x53756E53 - - -class SunspecModel: - """Assigned device indentifiers that are pre-assigned by the sunspec protocol.""" - - # --------------------------------------------- - # 0xx Common Models - # --------------------------------------------- - CommonBlock = 1 - AggregatorBlock = 2 - - # --------------------------------------------- - # 1xx Inverter Models - # --------------------------------------------- - SinglePhaseIntegerInverter = 101 - SplitPhaseIntegerInverter = 102 - ThreePhaseIntegerInverter = 103 - SinglePhaseFloatsInverter = 103 - SplitPhaseFloatsInverter = 102 - ThreePhaseFloatsInverter = 103 - - # --------------------------------------------- - # 2xx Meter Models - # --------------------------------------------- - SinglePhaseMeter = 201 - SplitPhaseMeter = 201 - WyeConnectMeter = 201 - DeltaConnectMeter = 201 - - # --------------------------------------------- - # 3xx Environmental Models - # --------------------------------------------- - BaseMeteorological = 301 - Irradiance = 302 - BackOfModuleTemperature = 303 - Inclinometer = 304 - Location = 305 - ReferencePoint = 306 - BaseMeteorological = 307 - MiniMeteorological = 308 - - # --------------------------------------------- - # 4xx String Combiner Models - # --------------------------------------------- - BasicStringCombiner = 401 - AdvancedStringCombiner = 402 - - # --------------------------------------------- - # 5xx Panel Models - # --------------------------------------------- - PanelFloat = 501 - PanelInteger = 502 - - # --------------------------------------------- - # 641xx outback_ Blocks - # --------------------------------------------- - outback_device_identifier = 64110 - outback_charge_controller = 64111 - outback_fm_charge_controller = 64112 - outback_fx_inv_realtime = 64113 - outback_fx_inv_conf = 64114 - outback_split_phase_rad_inv = 64115 - outback_radian_inv_conf = 64116 - outback_single_phase_rad_inv_rt = 64117 - outback_flexnet_dc_realtime = 64118 - outback_flexnet_dc_conf = 64119 - outback_system_control = 64120 - - # --------------------------------------------- - # 64xxx Vendor Extension Block - # --------------------------------------------- - EndOfSunSpecMap = 65535 - - @classmethod - def lookup(cls, code): - """Return the device model name for that identifier - - :param code: The device code to lookup - :returns: The device model name, or None if none available - """ - values = {(v, k) for k, v in cls.__dict__.iteritems() if not callable(v)} - return values.get(code, None) - - -class SunspecOffsets: - """Well known offsets that are used throughout the sunspec protocol""" - - CommonBlock = 40000 - CommonBlockLength = 69 - AlternateCommonBlock = 50000 - - -# --------------------------------------------------------------------------- # -# Common Functions -# --------------------------------------------------------------------------- # -def defer_or_apply(func): - """Apply an adapter method. - - to a result regardless if it is a deferred - or a concrete response. - - :param func: The function to decorate - """ - - def closure(future, adapt): - if isinstance(future, Deferred): - defer = Deferred() - future.addCallback(lambda r: defer.callback(adapt(r))) - return defer - return adapt(future) - - return closure - - -def create_sunspec_sync_client(host): - """Create a sunspec client. - - :param host: The host to connect to - :returns: an initialized SunspecClient - """ - modbus = ModbusTcpClient(host) - modbus.connect() - client = SunspecClient(modbus) - client.initialize() - return client - - -# --------------------------------------------------------------------------- # -# Sunspec Client -# --------------------------------------------------------------------------- # -class SunspecDecoder(BinaryPayloadDecoder): - """A decoder that deals correctly with the sunspec binary format.""" - - def __init__(self, payload, byteorder): - """Initialize a new instance of the SunspecDecoder - - .. note:: This is always set to big endian byte order - as specified in the protocol. - """ - my_byteorder = Endian.Big - BinaryPayloadDecoder.__init__(self, payload, my_byteorder) - - def decode_string(self, size=1): - """Decode a string from the buffer - - :param size: The size of the string to decode - """ - self._pointer += size - string = self._payload[self._pointer - size : self._pointer] - return string.split(SunspecDefaultValue.String)[0] - - -class SunspecClient: - """SunSpec client.""" - - def __init__(self, client): - """Initialize a new instance of the client - - :param client: The modbus client to use - """ - self.client = client - self.offset = SunspecOffsets.CommonBlock - - def initialize(self): - """Initialize the underlying client values - - :returns: True if successful, false otherwise - """ - decoder = self.get_device_block(self.offset, 2) - if decoder.decode_32bit_uint() == SunspecIdentifier.Sunspec: - return True - self.offset = SunspecOffsets.AlternateCommonBlock - decoder = self.get_device_block(self.offset, 2) - return decoder.decode_32bit_uint() == SunspecIdentifier.Sunspec - - def get_common_block(self): - """Read and return the sunspec common information block. - - :returns: A dictionary of the common block information - """ - length = SunspecOffsets.CommonBlockLength - decoder = self.get_device_block(self.offset, length) - return { - "SunSpec_ID": decoder.decode_32bit_uint(), - "SunSpec_DID": decoder.decode_16bit_uint(), - "SunSpec_Length": decoder.decode_16bit_uint(), - "Manufacturer": decoder.decode_string(size=32), - "Model": decoder.decode_string(size=32), - "Options": decoder.decode_string(size=16), - "Version": decoder.decode_string(size=16), - "SerialNumber": decoder.decode_string(size=32), - "DeviceAddress": decoder.decode_16bit_uint(), - "Next_DID": decoder.decode_16bit_uint(), - "Next_DID_Length": decoder.decode_16bit_uint(), - } - - def get_device_block(self, offset, size): - """Retrieve the next device block - - .. note:: We will read 2 more registers so that we have - the information for the next block. - - :param offset: The offset to start reading at - :param size: The size of the offset to read - :returns: An initialized decoder for that result - """ - txt = f"reading device block[{offset}..{offset + size}]" - _logger.debug(txt) - response = self.client.read_holding_registers(offset, size + 2) - return SunspecDecoder.fromRegisters(response.registers) - - def get_all_device_blocks(self): - """Retrieve all the available blocks in the supplied sunspec device. - - .. note:: Since we do not know how to decode the available - blocks, this returns a list of dictionaries of the form: - - decoder: the-binary-decoder, - model: the-model-identifier (name) - - :returns: A list of the available blocks - """ - blocks = [] - offset = self.offset + 2 - model = SunspecModel.CommonBlock - while model != SunspecModel.EndOfSunSpecMap: - decoder = self.get_device_block(offset, 2) - model = decoder.decode_16bit_uint() - length = decoder.decode_16bit_uint() - blocks.append( - { - "model": model, - "name": SunspecModel.lookup(model), - "length": length, - "offset": offset + length + 2, - } - ) - offset += length + 2 - return blocks - - -# ------------------------------------------------------------ -# A quick test runner -# ------------------------------------------------------------ -if __name__ == "__main__": - client = create_sunspec_sync_client("YOUR.HOST.GOES.HERE") - - # print out all the device common block - common = client.get_common_block() - for key, value in common.iteritems(): - if key == "SunSpec_DID": - value = SunspecModel.lookup(value) - print("{:<20}: {}".format(key, value)) - - # print out all the available device blocks - blocks = client.get_all_device_blocks() - for block in blocks: - print(block) - - client.client.close() diff --git a/examples/v2.5.3/tx_messages b/examples/v2.5.3/tx_messages deleted file mode 100644 index 994a92bd0..000000000 --- a/examples/v2.5.3/tx_messages +++ /dev/null @@ -1,215 +0,0 @@ -# ------------------------------------------------------------ -# What follows is a collection of encoded messages that can -# be used to test the message-parser. Simply uncomment the -# messages you want decoded and run the message parser with -# the given arguments. What follows is the listing of messages -# that are encoded in each format: -# -# - ReadHoldingRegistersRequest -# - ReadDiscreteInputsRequest -# - ReadInputRegistersRequest -# - ReadCoilsRequest -# - WriteMultipleCoilsRequest -# - WriteMultipleRegistersRequest -# - WriteSingleRegisterRequest -# - WriteSingleCoilRequest -# - ReadWriteMultipleRegistersRequest -# - ReadExceptionStatusRequest -# - GetCommEventCounterRequest -# - GetCommEventLogRequest -# - ReportSlaveIdRequest -# - ReadFileRecordRequest -# - WriteFileRecordRequest -# - MaskWriteRegisterRequest -# - ReadFifoQueueRequest -# - ReadDeviceInformationRequest -# - ReturnQueryDataRequest -# - RestartCommunicationsOptionRequest -# - ReturnDiagnosticRegisterRequest -# - ChangeAsciiInputDelimiterRequest -# - ForceListenOnlyModeRequest -# - ClearCountersRequest -# - ReturnBusMessageCountRequest -# - ReturnBusCommunicationErrorCountRequest -# - ReturnBusExceptionErrorCountRequest -# - ReturnSlaveMessageCountRequest -# - ReturnSlaveNoReponseCountRequest -# - ReturnSlaveNAKCountRequest -# - ReturnSlaveBusyCountRequest -# - ReturnSlaveBusCharacterOverrunCountRequest -# - ReturnIopOverrunCountRequest -# - ClearOverrunCountRequest -# - GetClearModbusPlusRequest -# ------------------------------------------------------------ -# Modbus TCP Messages -# ------------------------------------------------------------ -# [ MBAP Header ] [ Function Code] [ Data ] -# [ tid ][ pid ][ length ][ uid ] -# 2b 2b 2b 1b 1b Nb -# -# ./message-parser -b -p tcp -f messages -# ------------------------------------------------------------ -#000100000006010300120008 -#000100000006010200120008 -#000100000006010400120008 -#000100000006010100120008 -#000100000008010f0012000801ff -#0001000000170110001200081000010001000100010001000100010001 -#000100000006010600120001 -#00010000000601050012ff00 -#00010000001b011700120008000000081000010001000100010001000100010001 -#0001000000020107 -#000100000002010b -#000100000002010c -#0001000000020111 -#000100000003011400 -#000100000003011500 -#00010000000801160012ffff0000 -#00010000000401180012 -#000100000005012b0e0100 -#000100000006010800000000 -#000100000006010800010000 -#000100000006010800020000 -#000100000006010800030000 -#000100000006010800040000 -#0001000000060108000a0000 -#0001000000060108000b0000 -#0001000000060108000c0000 -#0001000000060108000d0000 -#0001000000060108000e0000 -#0001000000060108000f0000 -#000100000006010800100000 -#000100000006010800110000 -#000100000006010800120000 -#000100000006010800130000 -#000100000006010800140000 -#000100000006010800150000 -# ------------------------------------------------------------ -# Modbus RTU Messages -# ------------------------------------------------------------ -# [Address ][ Function Code] [ Data ][ CRC ] -# 1b 1b Nb 2b -# -# ./message-parser -b -p rtu -f messages -# ------------------------------------------------------------ -#010300120008e409 -#010200120008d9c9 -#01040012000851c9 -#0101001200089dc9 -#010f0012000801ff06d6 -#0110001200081000010001000100010001000100010001d551 -#010600120001e80f -#01050012ff002c3f -#011700120008000000081000010001000100010001000100010001e6f8 -#010741e2 -#010b41e7 -#010c0025 -#0111c02c -#0114002f00 -#0115002e90 -#01160012ffff00004e21 -#0118001201d2 -#012b0e01007077 -#010800000000e00b -#010800010000b1cb -#01080002000041cb -#010800030000100b -#010800040000a1ca -#0108000a0000c009 -#0108000b000091c9 -#0108000c00002008 -#0108000d000071c8 -#0108000e000081c8 -#0108000f0000d008 -#010800100000e1ce -#010800110000b00e -#010800120000400e -#01080013000011ce -#010800140000a00f -#010800150000f1cf -# ------------------------------------------------------------ -# Modbus ASCII Messages -# ------------------------------------------------------------ -# [ Start ][Address ][ Function ][ Data ][ LRC ][ End ] -# 1c 2c 2c Nc 2c 2c -# -# ./message-parser -a -p ascii -f messages -# ------------------------------------------------------------ -#:010300120008E2 -#:010200120008E3 -#:010400120008E1 -#:010100120008E4 -#:010F0012000801FFD6 -#:0110001200081000010001000100010001000100010001BD -#:010600120001E6 -#:01050012FF00E9 -#:011700120008000000081000010001000100010001000100010001AE -#:0107F8 -#:010BF4 -#:010CF3 -#:0111EE -#:011400EB -#:011500EA -#:01160012FFFF0000D9 -#:01180012D5 -#:012B0E0100C5 -#:010800000000F7 -#:010800010000F6 -#:010800020000F5 -#:010800030000F4 -#:010800040000F3 -#:0108000A0000ED -#:0108000B0000EC -#:0108000C0000EB -#:0108000D0000EA -#:0108000E0000E9 -#:0108000F0000E8 -#:010800100000E7 -#:010800110000E6 -#:010800120000E5 -#:010800130000E4 -#:010800140000E3 -#:010800150000E2 -# ------------------------------------------------------------ -# Modbus Binary Messages -# ------------------------------------------------------------ -# [ Start ][Address ][ Function ][ Data ][ CRC ][ End ] -# 1b 1b 1b Nb 2b 1b -# -# ./message-parser -b -p binary -f messages -# ------------------------------------------------------------ -#7b010300120008e4097d -#7b010200120008d9c97d -#7b01040012000851c97d -#7b0101001200089dc97d -#7b010f0012000801ff06d67d -#7b0110001200081000010001000100010001000100010001d5517d -#7b010600120001e80f7d -#7b01050012ff002c3f7d -#7b011700120008000000081000010001000100010001000100010001e6f87d -#7b010741e27d -#7b010b41e77d -#7b010c00257d -#7b0111c02c7d -#7b0114002f007d -#7b0115002e907d -#7b01160012ffff00004e217d -#7b0118001201d27d -#7b012b0e010070777d -#7b010800000000e00b7d -#7b010800010000b1cb7d -#7b01080002000041cb7d -#7b010800030000100b7d -#7b010800040000a1ca7d -#7b0108000a0000c0097d -#7b0108000b000091c97d -#7b0108000c000020087d -#7b0108000d000071c87d -#7b0108000e000081c87d -#7b0108000f0000d0087d -#7b010800100000e1ce7d -#7b010800110000b00e7d -#7b010800120000400e7d -#7b01080013000011ce7d -#7b010800140000a00f7d -#7b010800150000f1cf7d diff --git a/pymodbus/__init__.py b/pymodbus/__init__.py index c8a453346..48540146d 100644 --- a/pymodbus/__init__.py +++ b/pymodbus/__init__.py @@ -1,21 +1,16 @@ """Pymodbus: Modbus Protocol Implementation. -Released under the the BSD license +Released under the BSD license """ -from pymodbus.logging import pymodbus_apply_logging_config -from pymodbus.version import version - - -__version__ = version.short() -__version_full__ = str(version) - -# --------------------------------------------------------------------------- # -# Exported symbols -# --------------------------------------------------------------------------- # - __all__ = [ "pymodbus_apply_logging_config", "__version__", "__version_full__", ] + +from pymodbus.logging import pymodbus_apply_logging_config + + +__version__ = "3.3.0" +__version_full__ = f"[pymodbus, version {__version__}]" diff --git a/pymodbus/bit_read_message.py b/pymodbus/bit_read_message.py index 7ad992437..92876a172 100644 --- a/pymodbus/bit_read_message.py +++ b/pymodbus/bit_read_message.py @@ -1,4 +1,13 @@ """Bit Reading Request/Response messages.""" + +__all__ = [ + "ReadBitsResponseBase", + "ReadCoilsRequest", + "ReadCoilsResponse", + "ReadDiscreteInputsRequest", + "ReadDiscreteInputsResponse", +] + # pylint: disable=missing-type-doc import struct @@ -13,14 +22,14 @@ class ReadBitsRequestBase(ModbusRequest): _rtu_frame_size = 8 - def __init__(self, address, count, unit=Defaults.Slave, **kwargs): + def __init__(self, address, count, slave=Defaults.Slave, **kwargs): """Initialize the read request data. :param address: The start address to read from :param count: The number of bits after "address" to read - :param unit: Modbus slave unit ID + :param slave: Modbus slave slave ID """ - ModbusRequest.__init__(self, unit, **kwargs) + ModbusRequest.__init__(self, slave, **kwargs) self.address = address self.count = count @@ -67,13 +76,13 @@ class ReadBitsResponseBase(ModbusResponse): _rtu_byte_count_pos = 2 - def __init__(self, values, unit=Defaults.Slave, **kwargs): + def __init__(self, values, slave=Defaults.Slave, **kwargs): """Initialize a new instance. :param values: The requested values to be returned - :param unit: Modbus slave unit ID + :param slave: Modbus slave slave ID """ - ModbusResponse.__init__(self, unit, **kwargs) + ModbusResponse.__init__(self, slave, **kwargs) #: A list of booleans representing bit values self.bits = values or [] @@ -138,14 +147,14 @@ class ReadCoilsRequest(ReadBitsRequestBase): function_code = 1 function_code_name = "read_coils" - def __init__(self, address=None, count=None, unit=Defaults.Slave, **kwargs): + def __init__(self, address=None, count=None, slave=Defaults.Slave, **kwargs): """Initialize a new instance. :param address: The address to start reading from :param count: The number of bits to read - :param unit: Modbus slave unit ID + :param slave: Modbus slave slave ID """ - ReadBitsRequestBase.__init__(self, address, count, unit, **kwargs) + ReadBitsRequestBase.__init__(self, address, count, slave, **kwargs) def execute(self, context): """Run a read coils request against a datastore. @@ -183,13 +192,13 @@ class ReadCoilsResponse(ReadBitsResponseBase): function_code = 1 - def __init__(self, values=None, unit=Defaults.Slave, **kwargs): + def __init__(self, values=None, slave=Defaults.Slave, **kwargs): """Initialize a new instance. :param values: The request values to respond with - :param unit: Modbus slave unit ID + :param slave: Modbus slave slave ID """ - ReadBitsResponseBase.__init__(self, values, unit, **kwargs) + ReadBitsResponseBase.__init__(self, values, slave, **kwargs) class ReadDiscreteInputsRequest(ReadBitsRequestBase): @@ -204,14 +213,14 @@ class ReadDiscreteInputsRequest(ReadBitsRequestBase): function_code = 2 function_code_name = "read_discrete_input" - def __init__(self, address=None, count=None, unit=Defaults.Slave, **kwargs): + def __init__(self, address=None, count=None, slave=Defaults.Slave, **kwargs): """Initialize a new instance. :param address: The address to start reading from :param count: The number of bits to read - :param unit: Modbus slave unit ID + :param slave: Modbus slave slave ID """ - ReadBitsRequestBase.__init__(self, address, count, unit, **kwargs) + ReadBitsRequestBase.__init__(self, address, count, slave, **kwargs) def execute(self, context): """Run a read discrete input request against a datastore. @@ -249,22 +258,10 @@ class ReadDiscreteInputsResponse(ReadBitsResponseBase): function_code = 2 - def __init__(self, values=None, unit=Defaults.Slave, **kwargs): + def __init__(self, values=None, slave=Defaults.Slave, **kwargs): """Initialize a new instance. :param values: The request values to respond with - :param unit: Modbus slave unit ID + :param slave: Modbus slave slave ID """ - ReadBitsResponseBase.__init__(self, values, unit, **kwargs) - - -# ---------------------------------------------------------------------------# -# Exported symbols -# ---------------------------------------------------------------------------# -__all__ = [ - "ReadBitsResponseBase", - "ReadCoilsRequest", - "ReadCoilsResponse", - "ReadDiscreteInputsRequest", - "ReadDiscreteInputsResponse", -] + ReadBitsResponseBase.__init__(self, values, slave, **kwargs) diff --git a/pymodbus/bit_write_message.py b/pymodbus/bit_write_message.py index 7119387c6..5f5a71ad5 100644 --- a/pymodbus/bit_write_message.py +++ b/pymodbus/bit_write_message.py @@ -2,6 +2,14 @@ TODO write mask request/response """ + +__all__ = [ + "WriteSingleCoilRequest", + "WriteSingleCoilResponse", + "WriteMultipleCoilsRequest", + "WriteMultipleCoilsResponse", +] + # pylint: disable=missing-type-doc import struct @@ -41,13 +49,13 @@ class WriteSingleCoilRequest(ModbusRequest): _rtu_frame_size = 8 - def __init__(self, address=None, value=None, unit=None, **kwargs): + def __init__(self, address=None, value=None, slave=None, **kwargs): """Initialize a new instance. :param address: The variable address to write :param value: The value to write at address """ - ModbusRequest.__init__(self, unit=unit, **kwargs) + ModbusRequest.__init__(self, slave=slave, **kwargs) self.address = address self.value = bool(value) @@ -165,13 +173,13 @@ class WriteMultipleCoilsRequest(ModbusRequest): function_code_name = "write_coils" _rtu_byte_count_pos = 6 - def __init__(self, address=None, values=None, unit=None, **kwargs): + def __init__(self, address=None, values=None, slave=None, **kwargs): """Initialize a new instance. :param address: The starting request address :param values: The values to write """ - ModbusRequest.__init__(self, unit=unit, **kwargs) + ModbusRequest.__init__(self, slave=slave, **kwargs) self.address = address if not values: values = [] @@ -276,14 +284,3 @@ def __str__(self): :returns: A string representation of the instance """ return f"WriteNCoilResponse({self.address}, {self.count})" - - -# ---------------------------------------------------------------------------# -# Exported symbols -# ---------------------------------------------------------------------------# -__all__ = [ - "WriteSingleCoilRequest", - "WriteSingleCoilResponse", - "WriteMultipleCoilsRequest", - "WriteMultipleCoilsResponse", -] diff --git a/pymodbus/client/__init__.py b/pymodbus/client/__init__.py index 1d23fd39e..e9964c3dd 100644 --- a/pymodbus/client/__init__.py +++ b/pymodbus/client/__init__.py @@ -1,14 +1,5 @@ """Client""" -from pymodbus.client.base import ModbusBaseClient -from pymodbus.client.serial import AsyncModbusSerialClient, ModbusSerialClient -from pymodbus.client.tcp import AsyncModbusTcpClient, ModbusTcpClient -from pymodbus.client.tls import AsyncModbusTlsClient, ModbusTlsClient -from pymodbus.client.udp import AsyncModbusUdpClient, ModbusUdpClient - -# ---------------------------------------------------------------------------# -# Exported symbols -# ---------------------------------------------------------------------------# __all__ = [ "AsyncModbusSerialClient", "AsyncModbusTcpClient", @@ -20,3 +11,9 @@ "ModbusTlsClient", "ModbusUdpClient", ] + +from pymodbus.client.base import ModbusBaseClient +from pymodbus.client.serial import AsyncModbusSerialClient, ModbusSerialClient +from pymodbus.client.tcp import AsyncModbusTcpClient, ModbusTcpClient +from pymodbus.client.tls import AsyncModbusTlsClient, ModbusTlsClient +from pymodbus.client.udp import AsyncModbusUdpClient, ModbusUdpClient diff --git a/pymodbus/client/base.py b/pymodbus/client/base.py index 7f61b2441..39c3ca101 100644 --- a/pymodbus/client/base.py +++ b/pymodbus/client/base.py @@ -4,20 +4,21 @@ import asyncio import socket from dataclasses import dataclass -from typing import Any, Tuple, Type +from typing import Any, Callable from pymodbus.client.mixin import ModbusClientMixin from pymodbus.constants import Defaults -from pymodbus.exceptions import ConnectionException, NotImplementedException +from pymodbus.exceptions import ConnectionException from pymodbus.factory import ClientDecoder from pymodbus.framer import ModbusFramer from pymodbus.logging import Log from pymodbus.pdu import ModbusRequest, ModbusResponse from pymodbus.transaction import DictTransactionManager +from pymodbus.transport import BaseTransport from pymodbus.utilities import ModbusTransactionState -class ModbusBaseClient(ModbusClientMixin): +class ModbusBaseClient(ModbusClientMixin, BaseTransport): """**ModbusBaseClient** **Parameters common to all clients**: @@ -31,6 +32,7 @@ class ModbusBaseClient(ModbusClientMixin): :param broadcast_enable: (optional) True to treat id 0 as broadcast address. :param reconnect_delay: (optional) Minimum delay in milliseconds before reconnecting. :param reconnect_delay_max: (optional) Maximum delay in milliseconds before reconnecting. + :param on_reconnect_callback: (optional) Function that will be called just before a reconnection attempt. :param kwargs: (optional) Experimental parameters. .. tip:: @@ -38,27 +40,11 @@ class ModbusBaseClient(ModbusClientMixin): and not repeated with each client. .. tip:: - **delay_ms** doubles automatically with each unsuccessful connect, from + **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`, - unless you want to make a custom client. - - Custom client class **must** inherit :mod:`ModbusBaseClient`, example:: - - from pymodbus.client import ModbusBaseClient - - class myOwnClient(ModbusBaseClient): - - def __init__(self, **kwargs): - super().__init__(kwargs) - - def run(): - client = myOwnClient(...) - client.connect() - rr = client.read_coils(0x01) - client.close() + :mod:`ModbusBaseClient` is normally not referenced outside :mod:`pymodbus`. **Application methods, common to all clients**: """ @@ -69,7 +55,7 @@ class _params: # pylint: disable=too-many-instance-attributes host: str = None port: str | int = None - framer: Type[ModbusFramer] = None + framer: type[ModbusFramer] = None timeout: float = None retries: int = None retry_on_empty: bool = None @@ -78,7 +64,6 @@ class _params: # pylint: disable=too-many-instance-attributes broadcast_enable: bool = None kwargs: dict = None reconnect_delay: int = None - reconnect_delay_max: int = None baudrate: int = None bytesize: int = None @@ -86,7 +71,7 @@ class _params: # pylint: disable=too-many-instance-attributes stopbits: int = None handle_local_echo: bool = None - source_address: Tuple[str, int] = None + source_address: tuple[str, int] = None sslctx: str = None certfile: str = None @@ -94,20 +79,32 @@ class _params: # pylint: disable=too-many-instance-attributes password: str = None server_hostname: str = None - def __init__( + def __init__( # pylint: disable=too-many-arguments self, - framer: Type[ModbusFramer] = None, + framer: type[ModbusFramer] = None, timeout: str | float = Defaults.Timeout, retries: str | int = Defaults.Retries, retry_on_empty: bool = Defaults.RetryOnEmpty, close_comm_on_error: bool = Defaults.CloseCommOnError, strict: bool = Defaults.Strict, broadcast_enable: bool = Defaults.BroadcastEnable, - reconnect_delay: int = Defaults.ReconnectDelay, - reconnect_delay_max: int = Defaults.ReconnectDelayMax, + reconnect_delay: int = 0.1, + reconnect_delay_max: int = 300, + on_reconnect_callback: Callable[[], None] | None = None, **kwargs: Any, ) -> None: """Initialize a client instance.""" + BaseTransport.__init__( + self, + "comm", + (reconnect_delay * 1000, reconnect_delay_max * 1000), + timeout * 1000, + framer, + lambda: None, + self.cb_base_connection_lost, + self.cb_base_handle_data, + ) + self.framer = framer self.params = self._params() self.params.framer = framer self.params.timeout = float(timeout) @@ -117,25 +114,30 @@ def __init__( self.params.strict = bool(strict) self.params.broadcast_enable = bool(broadcast_enable) self.params.reconnect_delay = int(reconnect_delay) - self.params.reconnect_delay_max = int(reconnect_delay_max) + self.reconnect_delay_max = int(reconnect_delay_max) + self.on_reconnect_callback = on_reconnect_callback self.params.kwargs = kwargs + self.retry_on_empty: int = 0 + # -> retry read on nothing + + self.slaves: list[int] = [] + # -> list of acceptable slaves (0 for accept all) # Common variables. self.framer = self.params.framer(ClientDecoder(), self) self.transaction = DictTransactionManager( self, retries=retries, retry_on_empty=retry_on_empty, **kwargs ) - self.delay_ms = self.params.reconnect_delay - self.use_protocol = False - self._connected = False + self.reconnect_delay = self.params.reconnect_delay + self.reconnect_delay_current = self.params.reconnect_delay + self.use_sync = False self.use_udp = False self.state = ModbusTransactionState.IDLE self.last_frame_end: float = 0 self.silent_interval: float = 0 - self.transport = None # Initialize mixin - super().__init__() + ModbusClientMixin.__init__(self) # ----------------------------------------------------------------------- # # Client external interface @@ -151,19 +153,6 @@ def register(self, custom_response_class: ModbusResponse) -> None: """ self.framer.decoder.register(custom_response_class) - def connect(self): - """Connect to the modbus remote host (call **sync/async**). - - :raises ModbusException: Different exceptions, check exception text. - - **Remark** Retries are handled automatically after first successful connect. - """ - raise NotImplementedException - - def is_socket_open(self) -> bool: - """Return whether socket/serial is open or not (call **sync**).""" - raise NotImplementedException - def idle_time(self) -> float: """Time before initiating next transaction (call **sync**). @@ -174,10 +163,6 @@ def idle_time(self) -> float: return 0 return self.last_frame_end + self.silent_interval - def reset_delay(self) -> None: - """Reset wait time before next reconnect to minimal period (call **sync**).""" - self.delay_ms = self.params.reconnect_delay - def execute(self, request: ModbusRequest = None) -> ModbusResponse: """Execute request and get response (call **sync/async**). @@ -185,31 +170,17 @@ def execute(self, request: ModbusRequest = None) -> ModbusResponse: :returns: The result of the request execution :raises ConnectionException: Check exception text. """ - if self.use_protocol: - if not self._connected: - raise ConnectionException(f"Not connected[{str(self)}]") - return self.async_execute(request) - if not self.connect(): - raise ConnectionException(f"Failed to connect[{str(self)}]") - return self.transaction.execute(request) - - def close(self) -> None: - """Close the underlying socket connection (call **sync/async**).""" - raise NotImplementedException + if self.use_sync: + if not self.connect(): + raise ConnectionException(f"Failed to connect[{str(self)}]") + return self.transaction.execute(request) + if not self.transport: + raise ConnectionException(f"Not connected[{str(self)}]") + return self.async_execute(request) # ----------------------------------------------------------------------- # # Merged client methods # ----------------------------------------------------------------------- # - def client_made_connection(self, protocol): - """Run transport specific connection.""" - - def client_lost_connection(self, protocol): - """Run transport specific connection lost.""" - - def datagram_received(self, data, _addr): - """Receive datagram.""" - self.data_received(data) - async def async_execute(self, request=None): """Execute requests asynchronously.""" request.transaction_id = self.transaction.getNextTID() @@ -220,56 +191,35 @@ async def async_execute(self, request=None): else: self.transport.write(packet) req = self._build_response(request.transaction_id) - if self.params.broadcast_enable and not request.unit_id: + if self.params.broadcast_enable and not request.slave_id: resp = b"Broadcast write sent - no response expected" else: try: resp = await asyncio.wait_for(req, timeout=self.params.timeout) except asyncio.exceptions.TimeoutError: - self.connection_lost("trying to send") + self.close(reconnect=True) raise return resp - def connection_made(self, transport): - """Call when a connection is made. + def cb_base_handle_data(self, data: bytes) -> int: + """Handle received data - The transport argument is the transport representing the connection. + returns number of bytes consumed """ - self.transport = transport - Log.debug("Client connected to modbus server") - self._connected = True - self.client_made_connection(self) - - def connection_lost(self, reason): - """Call when the connection is lost or closed. + Log.debug("recv: {}", data, ":hex") + self.framer.processIncomingPacket(data, self._handle_response, slave=0) + return len(data) - The argument is either an exception object or None - """ - if self.transport: - self.transport.abort() - if hasattr(self.transport, "_sock"): - self.transport._sock.close() # pylint: disable=protected-access - self.transport = None - self.client_lost_connection(self) - Log.debug("Client disconnected from modbus server: {}", reason) - self._connected = False + def cb_base_connection_lost(self, _reason: Exception) -> None: + """Handle lost connection""" for tid in list(self.transaction): self.raise_future( self.transaction.getTransaction(tid), ConnectionException("Connection lost during request"), ) - def data_received(self, data): - """Call when some data is received. - - data is a non-empty bytes object containing the incoming data. - """ - Log.debug("recv: {}", data, ":hex") - self.framer.processIncomingPacket(data, self._handle_response, unit=0) - - def create_future(self): - """Help function to create asyncio Future object.""" - return asyncio.Future() + async def connect(self): + """Connect to the modbus remote host.""" def raise_future(self, my_future, exc): """Set exception of a future if not done.""" @@ -288,28 +238,17 @@ def _handle_response(self, reply, **_kwargs): def _build_response(self, tid): """Return a deferred response for the current request.""" - my_future = self.create_future() - if not self._connected: + my_future = asyncio.Future() + if not self.transport: self.raise_future(my_future, ConnectionException("Client is not connected")) else: self.transaction.addTransaction(my_future, tid) return my_future - @property - def async_connected(self): - """Return connection status.""" - return self._connected - - async def async_close(self): - """Close connection.""" - if self.transport: - self.transport.close() - self._connected = False - # ----------------------------------------------------------------------- # # Internal methods # ----------------------------------------------------------------------- # - def send(self, request): + def send(self, request): # pylint: disable=invalid-overridden-method """Send request. :meta private: @@ -331,7 +270,7 @@ def _get_address_family(cls, address): """Get the correct address family.""" try: _ = socket.inet_pton(socket.AF_INET6, address) - except socket.error: # not a valid ipv6 address + except OSError: # not a valid ipv6 address return socket.AF_INET return socket.AF_INET6 @@ -365,7 +304,7 @@ def __exit__(self, klass, value, traceback): async def __aexit__(self, klass, value, traceback): """Implement the client with exit block.""" - await self.close() + self.close() def __str__(self): """Build a string representation of the connection. diff --git a/pymodbus/client/mixin.py b/pymodbus/client/mixin.py index 7beaff677..ec11b3500 100644 --- a/pymodbus/client/mixin.py +++ b/pymodbus/client/mixin.py @@ -392,7 +392,7 @@ def write_registers( :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 unit ID + :param slave: (optional) Modbus slave ID :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ @@ -405,7 +405,7 @@ def write_registers( def report_slave_id(self, slave: int = 0, **kwargs: Any) -> ModbusResponse: """Report slave ID (code 0x11). - :param slave: (optional) Modbus slave unit ID + :param slave: (optional) Modbus slave ID :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ @@ -463,7 +463,7 @@ def readwrite_registers( :param read_count: The number of registers to read from address :param write_address: The address to start writing to :param values: List of values to write, or a single value to write - :param slave: (optional) Modbus slave unit ID + :param slave: (optional) Modbus slave ID :param kwargs: :raises ModbusException: """ @@ -472,8 +472,8 @@ def readwrite_registers( read_address=read_address, read_count=read_count, write_address=write_address, - values=values, - unit=slave, + write_registers=values, + slave=slave, **kwargs, ) ) @@ -508,7 +508,7 @@ def read_device_information( # ------------------ class DATATYPE(Enum): - """Datatype enum for convert_* calls.""" + """Datatype enum (name and number of bytes), used for convert_* calls.""" INT16 = ("h", 1) UINT16 = ("H", 1) @@ -528,7 +528,7 @@ def convert_from_registers( :param registers: list of registers received from e.g. read_holding_registers() :param data_type: data type to convert to - :returns: int, float or str depending on "to_type" + :returns: int, float or str depending on "data_type" :raises ModbusException: when size of registers is not 1, 2 or 4 """ byte_list = bytearray() @@ -550,12 +550,15 @@ def convert_to_registers( ) -> List[int]: """Convert int/float/str to registers (16/32/64 bit). - :param value: value to be converted: - :param data_type: data type to convert to + :param value: value to be converted + :param data_type: data type to be encoded as registers :returns: List of registers, can be used directly in e.g. write_registers() + :raises TypeError: when there is a mismatch between data_type and value """ if data_type == cls.DATATYPE.STRING: - byte_list = value.encode() # type: ignore[union-attr] + if not isinstance(value, str): + raise TypeError(f"Value should be string but is {type(value)}.") + byte_list = value.encode() if len(byte_list) % 2: byte_list += b"\x00" else: diff --git a/pymodbus/client/serial.py b/pymodbus/client/serial.py index 10df85fbc..901b662a0 100644 --- a/pymodbus/client/serial.py +++ b/pymodbus/client/serial.py @@ -1,11 +1,11 @@ """Modbus client async serial communication.""" import asyncio import time +from contextlib import suppress from functools import partial from typing import Any, Type from pymodbus.client.base import ModbusBaseClient -from pymodbus.client.serial_asyncio import create_serial_connection from pymodbus.constants import Defaults from pymodbus.exceptions import ConnectionException from pymodbus.framer import ModbusFramer @@ -14,10 +14,8 @@ from pymodbus.utilities import ModbusTransactionState -try: +with suppress(ImportError): import serial -except ImportError: - pass class AsyncModbusSerialClient(ModbusBaseClient, asyncio.Protocol): @@ -43,7 +41,7 @@ async def run(): await client.connect() ... - await client.close() + client.close() """ transport = None @@ -61,106 +59,30 @@ def __init__( **kwargs: Any, ) -> None: """Initialize Asyncio Modbus Serial Client.""" - super().__init__(framer=framer, **kwargs) - self.use_protocol = True + asyncio.Protocol.__init__(self) + ModbusBaseClient.__init__(self, framer=framer, **kwargs) self.params.port = port self.params.baudrate = baudrate self.params.bytesize = bytesize self.params.parity = parity self.params.stopbits = stopbits self.params.handle_local_echo = handle_local_echo - self.loop = None - self._connected_event = asyncio.Event() - self._reconnect_task = None - - async def close(self): # pylint: disable=invalid-overridden-method - """Stop connection.""" - - # prevent reconnect: - self.delay_ms = 0 - if self.connected: - if self.transport: - self.transport.close() - await self.async_close() - await asyncio.sleep(0.1) - - # if there is an unfinished delayed reconnection attempt pending, cancel it - if self._reconnect_task: - self._reconnect_task.cancel() - self._reconnect_task = None - - def _create_protocol(self): - """Create a protocol instance.""" - return self + self.setup_serial(False, port, baudrate, bytesize, parity, stopbits) @property def connected(self): """Connect internal.""" - return self._connected_event.is_set() + return self.transport is not None - async def connect(self): # pylint: disable=invalid-overridden-method + async def connect(self): """Connect Async client.""" - # get current loop, if there are no loop a RuntimeError will be raised - self.loop = asyncio.get_running_loop() - - Log.debug("Starting serial connection") - try: - await create_serial_connection( - self.loop, - self._create_protocol, - self.params.port, - baudrate=self.params.baudrate, - bytesize=self.params.bytesize, - stopbits=self.params.stopbits, - parity=self.params.parity, - timeout=self.params.timeout, - **self.params.kwargs, - ) - await self._connected_event.wait() - Log.info("Connected to {}", self.params.port) - except Exception as exc: # pylint: disable=broad-except - Log.warning("Failed to connect: {}", exc) - if self.delay_ms > 0: - self._launch_reconnect() - return self.connected - - def client_made_connection(self, protocol): - """Notify successful connection.""" - Log.info("Serial connected.") - if not self.connected: - self._connected_event.set() - else: - Log.error("Factory protocol connect callback called while connected.") - - def client_lost_connection(self, protocol): - """Notify lost connection.""" - Log.info("Serial lost connection.") - if protocol is not self: - Log.error("Serial: protocol is not self.") - - self._connected_event.clear() - if self.delay_ms: - self._launch_reconnect() - - def _launch_reconnect(self): - """Launch delayed reconnection coroutine""" - if self._reconnect_task: - Log.warning( - "Ignoring launch of delayed reconnection, another is in progress" - ) - else: - # store the future in a member variable so we know we have a pending reconnection attempt - # also prevents its garbage collection - self._reconnect_task = asyncio.create_task(self._reconnect()) + # if reconnect_delay_current was set to 0 by close(), we need to set it back again + # so this instance will work + self.reset_delay() - async def _reconnect(self): - """Reconnect.""" - Log.debug("Waiting {} ms before next connection attempt.", self.delay_ms) - await asyncio.sleep(self.delay_ms / 1000) - self.delay_ms = min(2 * self.delay_ms, self.params.reconnect_delay_max) - - self._reconnect_task = None - return await self.connect() + # force reconnect if required: + Log.debug("Connecting to {}.", self.params.host) + return await self.transport_connect() class ModbusSerialClient(ModbusBaseClient): @@ -216,10 +138,11 @@ def __init__( self.params.stopbits = stopbits self.params.handle_local_echo = handle_local_echo self.socket = None + self.use_sync = True self.last_frame_end = None - self._t0 = float((1 + 8 + 2)) / self.params.baudrate + self._t0 = float(1 + 8 + 2) / self.params.baudrate """ The minimum delay is 0.01s and the maximum can be set to 0.05s. @@ -231,20 +154,19 @@ def __init__( else 0.05 ) - if isinstance(self.framer, ModbusRtuFramer): - if self.params.baudrate > 19200: - self.silent_interval = 1.75 / 1000 # ms - else: - self.inter_char_timeout = 1.5 * self._t0 - self.silent_interval = 3.5 * self._t0 - self.silent_interval = round(self.silent_interval, 6) + if self.params.baudrate > 19200: + self.silent_interval = 1.75 / 1000 # ms + else: + self.inter_char_timeout = 1.5 * self._t0 + self.silent_interval = 3.5 * self._t0 + self.silent_interval = round(self.silent_interval, 6) @property def connected(self): """Connect internal.""" return self.connect() - def connect(self): + def connect(self): # pylint: disable=invalid-overridden-method """Connect to the modbus serial server.""" if self.socket: return True @@ -266,7 +188,7 @@ def connect(self): self.close() return self.socket is not None - def close(self): + def close(self): # pylint: disable=arguments-differ """Close the underlying socket connection.""" if self.socket: self.socket.close() diff --git a/pymodbus/client/sync_diag.py b/pymodbus/client/sync_diag.py deleted file mode 100644 index 36c0df4a3..000000000 --- a/pymodbus/client/sync_diag.py +++ /dev/null @@ -1,169 +0,0 @@ -"""Sync diag.""" -import socket -import time - -from pymodbus.client.tcp import ModbusTcpClient -from pymodbus.constants import Defaults -from pymodbus.exceptions import ConnectionException -from pymodbus.framer.socket_framer import ModbusSocketFramer -from pymodbus.logging import Log - - -LOG_MSGS = { - "conn_msg": "Connecting to modbus device %s", - "connfail_msg": "Connection to (%s, %s) failed: %s", - "discon_msg": "Disconnecting from modbus device %s", - "timelimit_read_msg": "Modbus device read took %.4f seconds, " - "returned %s bytes in timelimit read", - "timeout_msg": "Modbus device timeout after %.4f seconds, returned %s bytes %s", - "delay_msg": "Modbus device read took %.4f seconds, " - "returned %s bytes of %s expected", - "read_msg": "Modbus device read took %.4f seconds, " - "returned %s bytes of %s expected", - "unexpected_dc_msg": "%s %s", -} - - -class ModbusTcpDiagClient(ModbusTcpClient): - """Variant of pymodbus.client.ModbusTcpClient. - - With additional logging to diagnose network issues. - - The following events are logged: - - +---------+-----------------------------------------------------------------+ - | Level | Events | - +=========+=================================================================+ - | ERROR | Failure to connect to modbus unit; unexpected disconnect by | - | | modbus unit | - +---------+-----------------------------------------------------------------+ - | WARNING | Timeout on normal read; read took longer than warn_delay_limit | - +---------+-----------------------------------------------------------------+ - | INFO | Connection attempt to modbus unit; disconnection from modbus | - | | unit; each time limited read | - +---------+-----------------------------------------------------------------+ - | DEBUG | Normal read with timing information | - +---------+-----------------------------------------------------------------+ - - Reads are differentiated between "normal", which reads a specified number of - bytes, and "time limited", which reads all data for a duration equal to the - timeout period configured for this instance. - """ - - def __init__( - self, - host="127.0.0.1", - port=Defaults.TcpPort, - framer=ModbusSocketFramer, - **kwargs, - ): - """Initialize a client instance. - - The keys of LOG_MSGS can be used in kwargs to customize the messages. - - :param host: The host to connect to (default 127.0.0.1) - :param port: The modbus port to connect to (default 502) - :param source_address: The source address tuple to bind to (default ("", 0)) - :param timeout: The timeout to use for this socket (default Defaults.Timeout) - :param warn_delay_limit: Log reads that take longer than this as warning. - Default True sets it to half of "timeout". None never logs these as - warning, 0 logs everything as warning. - :param framer: The modbus framer to use (default ModbusSocketFramer) - - .. note:: The host argument will accept ipv4 and ipv6 hosts - """ - self.warn_delay_limit = kwargs.get("warn_delay_limit", True) - super().__init__(host, port, framer, **kwargs) - if self.warn_delay_limit is True: - self.warn_delay_limit = self.params.timeout / 2 - - # Set logging messages, defaulting to LOG_MSGS - for (k_item, v_item) in LOG_MSGS.items(): - self.__dict__[k_item] = kwargs.get(k_item, v_item) - - def connect(self): - """Connect to the modbus tcp server. - - :returns: True if connection succeeded, False otherwise - """ - if self.socket: - return True - try: - Log.info(LOG_MSGS["conn_msg"], self) - self.socket = socket.create_connection( - (self.params.host, self.params.port), - timeout=self.params.timeout, - source_address=self.params.source_address, - ) - except socket.error as msg: - Log.error(LOG_MSGS["connfail_msg"], self.params.host, self.params.port, msg) - self.close() - return self.socket is not None - - def close(self): - """Close the underlying socket connection.""" - if self.socket: - Log.info(LOG_MSGS["discon_msg"], self) - self.socket.close() - self.socket = None - - def recv(self, size): - """Receive data.""" - try: - start = time.time() - - result = super().recv(size) - - delay = time.time() - start - if self.warn_delay_limit is not None and delay >= self.warn_delay_limit: - self._log_delayed_response(len(result), size, delay) - elif not size: - Log.debug(LOG_MSGS["timelimit_read_msg"], delay, len(result)) - else: - Log.debug(LOG_MSGS["read_msg"], delay, len(result), size) - - return result - except ConnectionException as exc: - # Only log actual network errors, "if not self.socket" then it's a internal code issue - if "Connection unexpectedly closed" in exc.string: - Log.error(LOG_MSGS["unexpected_dc_msg"], self, exc) - raise ConnectionException from exc - - def _log_delayed_response(self, result_len, size, delay): - """Log delayed response.""" - if not size and result_len > 0: - Log.info(LOG_MSGS["timelimit_read_msg"], delay, result_len) - elif ( - (not result_len) or (size and result_len < size) - ) and delay >= self.params.timeout: - size_txt = size if size else "in timelimit read" - read_type = f"of {size_txt} expected" - Log.warning(LOG_MSGS["timeout_msg"], delay, result_len, read_type) - else: - Log.warning(LOG_MSGS["delay_msg"], delay, result_len, size) - - def __str__(self): - """Build a string representation of the connection. - - :returns: The string representation - """ - return f"ModbusTcpDiagClient({self.params.host}:{self.params.port})" - - -def get_client(): - """Return an appropriate client based on logging level. - - This will be ModbusTcpDiagClient by default, or the parent class - if the log level is such that the diagnostic client will not log - anything. - - :returns: ModbusTcpClient or a child class thereof - """ - return ModbusTcpDiagClient - - -# --------------------------------------------------------------------------- # -# Exported symbols -# --------------------------------------------------------------------------- # - -__all__ = ["ModbusTcpDiagClient", "get_client"] diff --git a/pymodbus/client/tcp.py b/pymodbus/client/tcp.py index 7346be817..304bf4017 100644 --- a/pymodbus/client/tcp.py +++ b/pymodbus/client/tcp.py @@ -34,7 +34,7 @@ async def run(): await client.connect() ... - await client.close() + client.close() """ def __init__( @@ -46,110 +46,33 @@ def __init__( **kwargs: Any, ) -> None: """Initialize Asyncio Modbus TCP Client.""" - super().__init__(framer=framer, **kwargs) - self.use_protocol = True + asyncio.Protocol.__init__(self) + ModbusBaseClient.__init__(self, framer=framer, **kwargs) self.params.host = host self.params.port = port self.params.source_address = source_address - self.loop = None - self.connected = False - self.delay_ms = self.params.reconnect_delay - self._reconnect_task = None + if "internal_no_setup" in kwargs: + return + if host.startswith("unix:"): + self.setup_unix(False, host[5:]) + else: + self.setup_tcp(False, host, port) - async def connect(self): # pylint: disable=invalid-overridden-method + async def connect(self): """Initiate connection to start client.""" - # if delay_ms was set to 0 by close(), we need to set it back again + # if reconnect_delay_current was set to 0 by close(), we need to set it back again # so this instance will work self.reset_delay() # force reconnect if required: - self.loop = asyncio.get_running_loop() Log.debug("Connecting to {}:{}.", self.params.host, self.params.port) - return await self._connect() - - async def close(self): # pylint: disable=invalid-overridden-method - """Stop client.""" - self.delay_ms = 0 - if self.connected: - if self.transport: - self.transport.abort() - self.transport.close() - await self.async_close() - await asyncio.sleep(0.1) - - if self._reconnect_task: - self._reconnect_task.cancel() - self._reconnect_task = None - - def _create_protocol(self): - """Create initialized protocol instance with function.""" - return self - - async def _connect(self): - """Connect.""" - Log.debug("Connecting.") - try: - if self.params.host.startswith("unix:"): - transport, protocol = await asyncio.wait_for( - self.loop.create_unix_connection( - self._create_protocol, path=self.params.host[5:] - ), - timeout=self.params.timeout, - ) - else: - transport, protocol = await asyncio.wait_for( - self.loop.create_connection( - self._create_protocol, - host=self.params.host, - port=self.params.port, - ), - timeout=self.params.timeout, - ) - except Exception as exc: # pylint: disable=broad-except - Log.warning("Failed to connect: {}", exc) - if self.delay_ms > 0: - self._launch_reconnect() - else: - Log.info("Connected to {}:{}.", self.params.host, self.params.port) - self.reset_delay() - return transport, protocol - - def client_made_connection(self, protocol): - """Notify successful connection.""" - Log.info("Protocol made connection.") - if not self.connected: - self.connected = True - else: - Log.error("Factory protocol connect callback called while connected.") - - def client_lost_connection(self, protocol): - """Notify lost connection.""" - Log.info("Protocol lost connection.") - if protocol is not self: - Log.error("Factory protocol cb from unknown protocol instance.") - - self.connected = False - if self.delay_ms > 0: - self._launch_reconnect() - - def _launch_reconnect(self): - """Launch delayed reconnection coroutine""" - if self._reconnect_task: - Log.warning( - "Ignoring launch of delayed reconnection, another is in progress" - ) - else: - self._reconnect_task = asyncio.create_task(self._reconnect()) - - async def _reconnect(self): - """Reconnect.""" - Log.debug("Waiting {} ms before next connection attempt.", self.delay_ms) - await asyncio.sleep(self.delay_ms / 1000) - self.delay_ms = min(2 * self.delay_ms, self.params.reconnect_delay_max) + return await self.transport_connect() - self._reconnect_task = None - return await self._connect() + @property + def connected(self): + """Return true if connected.""" + return self.transport is not None class ModbusTcpClient(ModbusBaseClient): @@ -191,13 +114,14 @@ def __init__( self.params.port = port self.params.source_address = source_address self.socket = None + self.use_sync = True @property def connected(self): """Connect internal.""" - return self.connect() + return self.transport is not None - def connect(self): + def connect(self): # pylint: disable=invalid-overridden-method """Connect to the modbus tcp server.""" if self.socket: return True @@ -216,7 +140,7 @@ def connect(self): "Connection to Modbus server established. Socket {}", self.socket.getsockname(), ) - except socket.error as msg: + except OSError as msg: Log.error( "Connection to ({}, {}) failed: {}", self.params.host, @@ -226,7 +150,7 @@ def connect(self): self.close() return self.socket is not None - def close(self): + def close(self): # pylint: disable=arguments-differ """Close the underlying socket connection.""" if self.socket: self.socket.close() @@ -309,9 +233,7 @@ def recv(self, size): return b"".join(data) - def _handle_abrupt_socket_close( - self, size, data, duration - ): # pylint: disable=missing-type-doc + def _handle_abrupt_socket_close(self, size, data, duration): """Handle unexpected socket close by remote end. Intended to be invoked after determining that the remote end @@ -338,7 +260,7 @@ def _handle_abrupt_socket_close( result = b"".join(data) Log.warning(" after returning {} bytes: {} ", len(result), result) return result - msg += " without response from unit before it closed connection" + msg += " without response from slave before it closed connection" raise ConnectionException(msg) def is_socket_open(self): diff --git a/pymodbus/client/tls.py b/pymodbus/client/tls.py index 305fe0c80..f724e3946 100644 --- a/pymodbus/client/tls.py +++ b/pymodbus/client/tls.py @@ -1,5 +1,4 @@ """Modbus client async TLS communication.""" -import asyncio import socket import ssl from typing import Any, Type @@ -40,7 +39,7 @@ def sslctx_provider( return sslctx -class AsyncModbusTlsClient(AsyncModbusTcpClient, asyncio.Protocol): +class AsyncModbusTlsClient(AsyncModbusTcpClient): """**AsyncModbusTlsClient**. :param host: Host IP address or host name @@ -54,6 +53,9 @@ class AsyncModbusTlsClient(AsyncModbusTcpClient, asyncio.Protocol): :param server_hostname: (optional) Bind certificate to host :param kwargs: (optional) Experimental parameters + ..tip:: + See ModbusBaseClient for common parameters. + Example:: from pymodbus.client import AsyncModbusTlsClient @@ -63,7 +65,7 @@ async def run(): await client.connect() ... - await client.close() + client.close() """ def __init__( @@ -71,7 +73,7 @@ def __init__( host: str, port: int = Defaults.TlsPort, framer: Type[ModbusFramer] = ModbusTlsFramer, - sslctx: str = None, + sslctx: ssl.SSLContext = None, certfile: str = None, keyfile: str = None, password: str = None, @@ -79,34 +81,29 @@ def __init__( **kwargs: Any, ): """Initialize Asyncio Modbus TLS Client.""" - super().__init__(host, port=port, framer=framer, **kwargs) + AsyncModbusTcpClient.__init__( + self, host, port=port, framer=framer, internal_no_setup=True, **kwargs + ) self.sslctx = sslctx_provider(sslctx, certfile, keyfile, password) - self.params.sslctx = sslctx self.params.certfile = certfile self.params.keyfile = keyfile self.params.password = password self.params.server_hostname = server_hostname - AsyncModbusTcpClient.__init__(self, host, port=port, framer=framer, **kwargs) + self.setup_tls( + False, host, port, sslctx, certfile, keyfile, password, server_hostname + ) - async def _connect(self): - """Connect to server.""" - Log.debug("Connecting tls.") - try: - return await self.loop.create_connection( - self._create_protocol, - self.params.host, - self.params.port, - ssl=self.sslctx, - server_hostname=self.params.server_hostname, - ) - except Exception as exc: # pylint: disable=broad-except - Log.warning("Failed to connect: {}", exc) - if self.delay_ms > 0: - self._launch_reconnect() - return - Log.info("Connected to {}:{}.", self.params.host, self.params.port) + async def connect(self): + """Initiate connection to start client.""" + + # if reconnect_delay_current was set to 0 by close(), we need to set it back again + # so this instance will work self.reset_delay() + # force reconnect if required: + Log.debug("Connecting to {}:{}.", self.params.host, self.params.port) + return await self.transport_connect() + class ModbusTlsClient(ModbusTcpClient): """**ModbusTlsClient**. @@ -122,6 +119,9 @@ class ModbusTlsClient(ModbusTcpClient): :param server_hostname: (optional) Bind certificate to host :param kwargs: (optional) Experimental parameters + ..tip:: + See ModbusBaseClient for common parameters. + Example:: from pymodbus.client import ModbusTlsClient @@ -161,7 +161,7 @@ def __init__( @property def connected(self): """Connect internal.""" - return self.connect() + return self.transport is not None def connect(self): """Connect to the modbus tls server.""" @@ -176,7 +176,7 @@ def connect(self): ) self.socket.settimeout(self.params.timeout) self.socket.connect((self.params.host, self.params.port)) - except socket.error as msg: + except OSError as msg: Log.error( "Connection to ({}, {}) failed: {}", self.params.host, diff --git a/pymodbus/client/udp.py b/pymodbus/client/udp.py index 04f3969bd..3b103c73f 100644 --- a/pymodbus/client/udp.py +++ b/pymodbus/client/udp.py @@ -25,6 +25,9 @@ class AsyncModbusUdpClient( :param source_address: (optional) source address of client, :param kwargs: (optional) Experimental parameters + ..tip:: + See ModbusBaseClient for common parameters. + Example:: from pymodbus.client import AsyncModbusUdpClient @@ -34,7 +37,7 @@ async def run(): await client.connect() ... - await client.close() + client.close() """ def __init__( @@ -46,110 +49,30 @@ def __init__( **kwargs: Any, ) -> None: """Initialize Asyncio Modbus UDP Client.""" - super().__init__(framer=framer, **kwargs) - self.use_protocol = True - self.params.host = host + asyncio.DatagramProtocol.__init__(self) + asyncio.Protocol.__init__(self) + ModbusBaseClient.__init__(self, framer=framer, **kwargs) self.params.port = port self.params.source_address = source_address - self._reconnect_task = None - self.loop = asyncio.get_event_loop() - self.connected = False - self.delay_ms = self.params.reconnect_delay - self._reconnect_task = None - self.reset_delay() - - async def connect(self): # pylint: disable=invalid-overridden-method - """Start reconnecting asynchronous udp client. + self.setup_udp(False, host, port) - :meta private: - """ - # get current loop, if there are no loop a RuntimeError will be raised - self.loop = asyncio.get_running_loop() - Log.debug("Connecting to {}:{}.", self.params.host, self.params.port) - - # getaddrinfo returns a list of tuples - # - [(family, type, proto, canonname, sockaddr),] - # We want sockaddr which is a (ip, port) tuple - # udp needs ip addresses, not hostnames - # TBD: addrinfo = await self.loop.getaddrinfo(self.params.host, self.params.port, type=DGRAM_TYPE) - # TBD: self.params.host, self.params.port = addrinfo[-1][-1] - return await self._connect() - - async def close(self): # pylint: disable=invalid-overridden-method - """Stop connection and prevents reconnect. + @property + def connected(self): + """Return true if connected.""" + return self.transport is not None - :meta private: - """ - self.delay_ms = 0 - if self.connected: - if self.transport: - self.transport.abort() - self.transport.close() - await self.async_close() - await asyncio.sleep(0.1) - - if self._reconnect_task: - self._reconnect_task.cancel() - self._reconnect_task = None - - def _create_protocol(self): - """Create initialized protocol instance with function.""" - self.use_udp = True - return self - - async def _connect(self): - """Connect.""" - Log.debug("Connecting.") - try: - endpoint = await self.loop.create_datagram_endpoint( - self._create_protocol, - remote_addr=(self.params.host, self.params.port), - ) - Log.info("Connected to {}:{}.", self.params.host, self.params.port) - return endpoint - except Exception as exc: # pylint: disable=broad-except - Log.warning("Failed to connect: {}", exc) - self._reconnect_task = asyncio.ensure_future(self._reconnect()) - - def client_made_connection(self, protocol): - """Notify successful connection. + async def connect(self): + """Start reconnecting asynchronous udp client. :meta private: """ - Log.info("Protocol made connection.") - if not self.connected: - self.connected = True - else: - Log.error("Factory protocol connect callback called while connected.") - - def client_lost_connection(self, protocol): - """Notify lost connection. + # if reconnect_delay_current was set to 0 by close(), we need to set it back again + # so this instance will work + self.reset_delay() - :meta private: - """ - Log.info("Protocol lost connection.") - if protocol is not self: - Log.error("Factory protocol cb from unexpected protocol instance.") - - self.connected = False - if self.delay_ms > 0: - self._launch_reconnect() - - def _launch_reconnect(self): - """Launch delayed reconnection coroutine""" - if self._reconnect_task: - Log.warning( - "Ignoring launch of delayed reconnection, another is in progress" - ) - else: - self._reconnect_task = asyncio.create_task(self._reconnect()) - - async def _reconnect(self): - """Reconnect.""" - Log.debug("Waiting {} ms before next connection attempt.", self.delay_ms) - await asyncio.sleep(self.delay_ms / 1000) - self.delay_ms = 2 * self.delay_ms - return await self._connect() + # force reconnect if required: + Log.debug("Connecting to {}:{}.", self.comm_params.host, self.comm_params.port) + return await self.transport_connect() class ModbusUdpClient(ModbusBaseClient): @@ -161,6 +84,9 @@ class ModbusUdpClient(ModbusBaseClient): :param source_address: (optional) source address of client, :param kwargs: (optional) Experimental parameters + ..tip:: + See ModbusBaseClient for common parameters. + Example:: from pymodbus.client import ModbusUdpClient @@ -190,16 +116,9 @@ def __init__( self.params.source_address = source_address self.socket = None + self.use_sync = True - @property - def connected(self): - """Connect internal. - - :meta private: - """ - return self.connect() - - def connect(self): + def connect(self): # pylint: disable=invalid-overridden-method """Connect to the modbus tcp server. :meta private: @@ -210,12 +129,12 @@ def connect(self): family = ModbusUdpClient._get_address_family(self.params.host) self.socket = socket.socket(family, socket.SOCK_DGRAM) self.socket.settimeout(self.params.timeout) - except socket.error as exc: + except OSError as exc: Log.error("Unable to create udp socket {}", exc) self.close() return self.socket is not None - def close(self): + def close(self): # pylint: disable=arguments-differ """Close the underlying socket connection. :meta private: @@ -249,9 +168,7 @@ def is_socket_open(self): :meta private: """ - if self.socket: - return True - return self.connect() + return True def __str__(self): """Build a string representation of the connection.""" diff --git a/pymodbus/constants.py b/pymodbus/constants.py index 66a5b5306..5dea1d26b 100644 --- a/pymodbus/constants.py +++ b/pymodbus/constants.py @@ -104,8 +104,8 @@ class Defaults: # pylint: disable=too-few-public-methods .. attribute:: broadcastEnable - When False unit_id 0 will be treated as any other unit_id. When True and - the unit_id is 0 the server will execute all requests on all server + When False slave_id 0 will be treated as any other slave_id. When True and + the slave_id is 0 the server will execute all requests on all server contexts and not respond and the client will skip trying to receive a response. Default value False does not conform to Modbus spec but maintains legacy behavior for existing pymodbus users. @@ -191,7 +191,7 @@ class ModbusStatus: # pylint: disable=too-few-public-methods Waiting = 0xFFFF Ready = 0x0000 - On = 0xFF00 # pylint: disable=invalid-name + On = 0xFF00 Off = 0x0000 SlaveOn = 0xFF SlaveOff = 0x00 @@ -307,16 +307,3 @@ class MoreData: # pylint: disable=too-few-public-methods def __init__(self): """Prohibit objects.""" raise RuntimeError(INTERNAL_ERROR) - - -# ---------------------------------------------------------------------------# -# Exported Identifiers -# ---------------------------------------------------------------------------# -__all__ = [ - "Defaults", - "ModbusStatus", - "Endian", - "ModbusPlusOperation", - "DeviceInformation", - "MoreData", -] diff --git a/pymodbus/datastore/__init__.py b/pymodbus/datastore/__init__.py index 71efa2101..e66600d7d 100644 --- a/pymodbus/datastore/__init__.py +++ b/pymodbus/datastore/__init__.py @@ -1,28 +1,21 @@ """Datastore.""" + +__all__ = [ + "ModbusBaseSlaveContext", + "ModbusSequentialDataBlock", + "ModbusSparseDataBlock", + "ModbusSlaveContext", + "ModbusServerContext", + "ModbusSimulatorContext", +] + from pymodbus.datastore.context import ( ModbusBaseSlaveContext, ModbusServerContext, ModbusSlaveContext, ) -from pymodbus.datastore.database.redis_datastore import RedisSlaveContext -from pymodbus.datastore.database.sql_datastore import SqlSlaveContext from pymodbus.datastore.simulator import ModbusSimulatorContext from pymodbus.datastore.store import ( ModbusSequentialDataBlock, ModbusSparseDataBlock, ) - - -# ---------------------------------------------------------------------------# -# Exported symbols -# ---------------------------------------------------------------------------# -__all__ = [ - "ModbusBaseSlaveContext", - "ModbusSequentialDataBlock", - "ModbusSparseDataBlock", - "ModbusSlaveContext", - "ModbusServerContext", - "ModbusSimulatorContext", - "RedisSlaveContext", - "SqlSlaveContext", -] diff --git a/pymodbus/datastore/context.py b/pymodbus/datastore/context.py index c1c5fe5ec..0462ab984 100644 --- a/pymodbus/datastore/context.py +++ b/pymodbus/datastore/context.py @@ -76,7 +76,7 @@ def validate(self, fc_as_hex, address, count=1): :returns: True if the request in within range, False otherwise """ if not self.zero_mode: - address = address + 1 + address += 1 Log.debug("validate: fc-[{}] address-{}: count-{}", fc_as_hex, address, count) return self.store[self.decode(fc_as_hex)].validate(address, count) @@ -89,7 +89,7 @@ def getValues(self, fc_as_hex, address, count=1): :returns: The requested values from a:a+c """ if not self.zero_mode: - address = address + 1 + address += 1 Log.debug("getValues: fc-[{}] address-{}: count-{}", fc_as_hex, address, count) return self.store[self.decode(fc_as_hex)].getValues(address, count) @@ -101,7 +101,7 @@ def setValues(self, fc_as_hex, address, values): :param values: The new values to be set """ if not self.zero_mode: - address = address + 1 + address += 1 Log.debug("setValues[{}] address-{}: count-{}", fc_as_hex, address, len(values)) self.store[self.decode(fc_as_hex)].setValues(address, values) @@ -120,7 +120,7 @@ class ModbusServerContext: """This represents a master collection of slave contexts. If single is set to true, it will be treated as a single - context so every unit-id returns the same context. If single + context so every slave_id returns the same context. If single is set to false, it will be interpreted as a collection of slave contexts. """ diff --git a/pymodbus/datastore/database/__init__.py b/pymodbus/datastore/database/__init__.py deleted file mode 100644 index f84e27360..000000000 --- a/pymodbus/datastore/database/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Define Datastore.""" -from pymodbus.datastore.database.redis_datastore import RedisSlaveContext -from pymodbus.datastore.database.sql_datastore import SqlSlaveContext - - -# ---------------------------------------------------------------------------# -# Exported symbols -# ---------------------------------------------------------------------------# -__all__ = ["SqlSlaveContext", "RedisSlaveContext"] diff --git a/pymodbus/datastore/remote.py b/pymodbus/datastore/remote.py index 531eb26b5..2eeb1b4ca 100644 --- a/pymodbus/datastore/remote.py +++ b/pymodbus/datastore/remote.py @@ -15,14 +15,14 @@ class RemoteSlaveContext(ModbusBaseSlaveContext): a remote device (depending on the client used) """ - def __init__(self, client, unit=None): + def __init__(self, client, slave=None): """Initialize the datastores. :param client: The client to retrieve values with - :param unit: Unit ID of the remote slave + :param slave: Unit ID of the remote slave """ self._client = client - self.unit = unit + self.slave = slave self.result = None self.__build_mapping() if not self.__set_callbacks: @@ -71,8 +71,8 @@ def __str__(self): def __build_mapping(self): """Build the function code mapper.""" kwargs = {} - if self.unit: - kwargs["slave"] = self.unit + if self.slave: + kwargs["slave"] = self.slave self.__get_callbacks = { "d": lambda a, c: self._client.read_discrete_inputs( # pylint: disable=unnecessary-lambda a, c, **kwargs diff --git a/pymodbus/datastore/simulator.py b/pymodbus/datastore/simulator.py index 7fa762401..cd9cade56 100755 --- a/pymodbus/datastore/simulator.py +++ b/pymodbus/datastore/simulator.py @@ -554,8 +554,9 @@ def validate(self, func_code, address, count=1): # Bit count, correct to register count count = int((count + WORD_SIZE - 1) / WORD_SIZE) address = int(address / 16) + real_address = self.fc_offset[func_code] + address - if real_address <= 0 or real_address > self.register_count: + if real_address < 0 or real_address > self.register_count: return False fx_write = func_code in self._write_func_code diff --git a/pymodbus/device.py b/pymodbus/device.py index dabf81529..fa4bff8bf 100644 --- a/pymodbus/device.py +++ b/pymodbus/device.py @@ -4,6 +4,13 @@ maintained in the server context and the various methods should be inserted in the correct locations. """ + +__all__ = [ + "ModbusPlusStatistics", + "ModbusDeviceIdentification", + "DeviceInformationFactory", +] + import struct # pylint: disable=missing-type-doc @@ -457,7 +464,7 @@ class ModbusControlBlock: __mode = "ASCII" __diagnostic = [False] * 16 __listen_only = False - __delimiter = "\r" + __delimiter = b"\r" __counters = ModbusCountersHandler() __identity = ModbusDeviceIdentification() __plus = ModbusPlusStatistics() @@ -599,14 +606,3 @@ def getDiagnosticRegister(self): :returns: The diagnostic register collection """ return self.__diagnostic - - -# ---------------------------------------------------------------------------# -# Exported Identifiers -# ---------------------------------------------------------------------------# -__all__ = [ - "ModbusPlusStatistics", - "ModbusDeviceIdentification", - "DeviceInformationFactory", - "ModbusControlBlock", -] diff --git a/pymodbus/diag_message.py b/pymodbus/diag_message.py index 156002ce1..705b01977 100644 --- a/pymodbus/diag_message.py +++ b/pymodbus/diag_message.py @@ -3,6 +3,46 @@ These need to be tied into a the current server context 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 @@ -106,7 +146,7 @@ def encode(self): packet += self.message.encode() elif isinstance(self.message, bytes): packet += self.message - elif isinstance(self.message, list): + elif isinstance(self.message, (list, tuple)): for piece in self.message: packet += struct.pack(">H", piece) elif isinstance(self.message, int): @@ -121,7 +161,7 @@ def decode(self, data): word_len = len(data) // 2 if len(data) % 2: word_len += 1 - data = data + b"0" + data += b"0" data = struct.unpack(">" + "H" * word_len, data) ( self.sub_function_code, # pylint: disable=attribute-defined-outside-init @@ -191,12 +231,12 @@ class ReturnQueryDataRequest(DiagnosticStatusRequest): sub_function_code = 0x0000 - def __init__(self, message=0x0000, unit=None, **kwargs): + def __init__(self, message=0x0000, slave=None, **kwargs): """Initialize a new instance of the request. :param message: The message to send to loopback """ - DiagnosticStatusRequest.__init__(self, unit=unit, **kwargs) + DiagnosticStatusRequest.__init__(self, slave=slave, **kwargs) if isinstance(message, list): self.message = message else: @@ -248,12 +288,12 @@ class RestartCommunicationsOptionRequest(DiagnosticStatusRequest): sub_function_code = 0x0001 - def __init__(self, toggle=False, unit=None, **kwargs): + def __init__(self, toggle=False, slave=None, **kwargs): """Initialize a new request. :param toggle: Set to True to toggle, False otherwise """ - DiagnosticStatusRequest.__init__(self, unit=unit, **kwargs) + DiagnosticStatusRequest.__init__(self, slave=slave, **kwargs) if toggle: self.message = [ModbusStatus.On] else: @@ -774,9 +814,9 @@ class GetClearModbusPlusRequest(DiagnosticStatusSimpleRequest): sub_function_code = 0x0015 - def __init__(self, unit=None, **kwargs): + def __init__(self, slave=None, **kwargs): """Initialize.""" - super().__init__(unit=unit, **kwargs) + super().__init__(slave=slave, **kwargs) def get_response_pdu_size(self): """Return a series of 54 16-bit words (108 bytes) in the data field of the response. @@ -826,46 +866,3 @@ class GetClearModbusPlusResponse(DiagnosticStatusSimpleResponse): """ sub_function_code = 0x0015 - - -# ---------------------------------------------------------------------------# -# Exported symbols -# ---------------------------------------------------------------------------# -__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", -] diff --git a/pymodbus/events.py b/pymodbus/events.py index ad0b23ded..8a66ed10a 100644 --- a/pymodbus/events.py +++ b/pymodbus/events.py @@ -17,7 +17,7 @@ def encode(self): :raises NotImplementedException: """ - raise NotImplementedException() + raise NotImplementedException def decode(self, event): """Decode the event message to its status bits. @@ -25,7 +25,7 @@ def decode(self, event): :param event: The event to decode :raises NotImplementedException: """ - raise NotImplementedException() + raise NotImplementedException class RemoteReceiveEvent(ModbusEvent): @@ -54,7 +54,7 @@ def __init__(self, **kwargs): self.listen = kwargs.get("listen", False) self.broadcast = kwargs.get("broadcast", False) - def encode(self): + def encode(self) -> bytes: """Encode the status bits to an event message. :returns: The encoded event message @@ -64,7 +64,7 @@ def encode(self): packet = pack_bitstring(bits) return packet - def decode(self, event): + def decode(self, event: bytes) -> None: """Decode the event message to its status bits. :param event: The event to decode diff --git a/pymodbus/exceptions.py b/pymodbus/exceptions.py index eee86139f..efc276a22 100644 --- a/pymodbus/exceptions.py +++ b/pymodbus/exceptions.py @@ -3,6 +3,17 @@ Custom exceptions to be used in the Modbus code. """ +__all__ = [ + "ModbusException", + "ModbusIOException", + "ParameterException", + "NotImplementedException", + "ConnectionException", + "NoSuchSlaveException", + "InvalidMessageReceivedException", + "MessageRegisterException", +] + class ModbusException(Exception): """Base modbus exception.""" @@ -104,18 +115,3 @@ def __init__(self, string=""): """Initialize.""" message = f"[Error registering message] {string}" ModbusException.__init__(self, message) - - -# --------------------------------------------------------------------------- # -# Exported symbols -# --------------------------------------------------------------------------- # -__all__ = [ - "ModbusException", - "ModbusIOException", - "ParameterException", - "NotImplementedException", - "ConnectionException", - "NoSuchSlaveException", - "InvalidMessageReceivedException", - "MessageRegisterException", -] diff --git a/pymodbus/factory.py b/pymodbus/factory.py index 1c03144ca..73fdcb0d9 100644 --- a/pymodbus/factory.py +++ b/pymodbus/factory.py @@ -8,6 +8,7 @@ Regardless of how many functions are added to the lookup, O(1) behavior is kept as a result of a pre-computed lookup dictionary. """ + # pylint: disable=missing-type-doc from pymodbus.bit_read_message import ( ReadCoilsRequest, @@ -83,9 +84,13 @@ ReportSlaveIdRequest, ReportSlaveIdResponse, ) -from pymodbus.pdu import ExceptionResponse, IllegalFunctionRequest +from pymodbus.pdu import ( + ExceptionResponse, + IllegalFunctionRequest, + ModbusRequest, + ModbusResponse, +) from pymodbus.pdu import ModbusExceptions as ecode -from pymodbus.pdu import ModbusRequest, ModbusResponse from pymodbus.register_read_message import ( ReadHoldingRegistersRequest, ReadHoldingRegistersResponse, @@ -163,7 +168,7 @@ def getFCdict(cls): def __init__(self): """Initialize the client lookup tables.""" functions = {f.function_code for f in self.__function_table} - self.__lookup = self.getFCdict() + self.lookup = self.getFCdict() self.__sub_lookup = {f: {} for f in functions} for f in self.__sub_function_table: self.__sub_lookup[f.function_code][f.sub_function_code] = f @@ -186,7 +191,7 @@ def lookupPduClass(self, function_code): :param function_code: The function code specified in a frame. :returns: The class of the PDU that has a matching `function_code`. """ - return self.__lookup.get(function_code, ExceptionResponse) + return self.lookup.get(function_code, ExceptionResponse) def _helper(self, data): """Generate the correct request object from a valid request packet. @@ -197,12 +202,12 @@ def _helper(self, data): :returns: The decoded request or illegal function request object """ function_code = int(data[0]) - if not (request := self.__lookup.get(function_code, lambda: None)()): + if not (request := self.lookup.get(function_code, lambda: None)()): Log.debug("Factory Request[{}]", function_code) request = IllegalFunctionRequest(function_code) else: - fc_string = "%s: %s" % ( # pylint: disable=consider-using-f-string - str(self.__lookup[function_code]) # pylint: disable=use-maxsplit-arg + fc_string = "{}: {}".format( # pylint: disable=consider-using-f-string + str(self.lookup[function_code]) # pylint: disable=use-maxsplit-arg .split(".")[-1] .rstrip('">"'), function_code, @@ -229,7 +234,7 @@ def register(self, function=None): ". Class needs to be derived from " "`pymodbus.pdu.ModbusRequest` " ) - self.__lookup[function.function_code] = function + self.lookup[function.function_code] = function if hasattr(function, "sub_function_code"): if function.function_code not in self.__sub_lookup: self.__sub_lookup[function.function_code] = {} @@ -292,7 +297,7 @@ class ClientDecoder: def __init__(self): """Initialize the client lookup tables.""" functions = {f.function_code for f in self.function_table} - self.__lookup = {f.function_code: f for f in self.function_table} + self.lookup = {f.function_code: f for f in self.function_table} self.__sub_lookup = {f: {} for f in functions} for f in self.__sub_function_table: self.__sub_lookup[f.function_code][f.sub_function_code] = f @@ -303,7 +308,7 @@ def lookupPduClass(self, function_code): :param function_code: The function code specified in a frame. :returns: The class of the PDU that has a matching `function_code`. """ - return self.__lookup.get(function_code, ExceptionResponse) + return self.lookup.get(function_code, ExceptionResponse) def decode(self, message): """Decode a response packet. @@ -329,15 +334,15 @@ def _helper(self, data): :raises ModbusException: """ fc_string = function_code = int(data[0]) - if function_code in self.__lookup: - fc_string = "%s: %s" % ( # pylint: disable=consider-using-f-string - str(self.__lookup[function_code]) # pylint: disable=use-maxsplit-arg + if function_code in self.lookup: + fc_string = "{}: {}".format( # pylint: disable=consider-using-f-string + str(self.lookup[function_code]) # pylint: disable=use-maxsplit-arg .split(".")[-1] .rstrip('">"'), function_code, ) Log.debug("Factory Response[{}]", fc_string) - response = self.__lookup.get(function_code, lambda: None)() + response = self.lookup.get(function_code, lambda: None)() if function_code > 0x80: code = function_code & 0x7F # strip error portion response = ExceptionResponse(code, ecode.IllegalFunction) @@ -360,18 +365,10 @@ def register(self, function): ". Class needs to be derived from " "`pymodbus.pdu.ModbusResponse` " ) - self.__lookup[function.function_code] = function + self.lookup[function.function_code] = function if hasattr(function, "sub_function_code"): if function.function_code not in self.__sub_lookup: self.__sub_lookup[function.function_code] = {} self.__sub_lookup[function.function_code][ function.sub_function_code ] = function - - -# --------------------------------------------------------------------------- # -# Exported symbols -# --------------------------------------------------------------------------- # - - -__all__ = ["ServerDecoder", "ClientDecoder"] diff --git a/pymodbus/file_message.py b/pymodbus/file_message.py index aacab7918..45e2fdc90 100644 --- a/pymodbus/file_message.py +++ b/pymodbus/file_message.py @@ -2,6 +2,17 @@ Currently none of these messages are implemented """ + +__all__ = [ + "FileRecord", + "ReadFileRecordRequest", + "ReadFileRecordResponse", + "WriteFileRecordRequest", + "WriteFileRecordResponse", + "ReadFifoQueueRequest", + "ReadFifoQueueResponse", +] + # pylint: disable=missing-type-doc import struct @@ -424,17 +435,3 @@ def decode(self, data): for index in range(0, count - 4): idx = 4 + index * 2 self.values.append(struct.unpack(">H", data[idx : idx + 2])[0]) - - -# ---------------------------------------------------------------------------# -# Exported symbols -# ---------------------------------------------------------------------------# -__all__ = [ - "FileRecord", - "ReadFileRecordRequest", - "ReadFileRecordResponse", - "WriteFileRecordRequest", - "WriteFileRecordResponse", - "ReadFifoQueueRequest", - "ReadFifoQueueResponse", -] diff --git a/pymodbus/framer/__init__.py b/pymodbus/framer/__init__.py index d29777424..94c840e5e 100644 --- a/pymodbus/framer/__init__.py +++ b/pymodbus/framer/__init__.py @@ -1,60 +1,17 @@ -"""Framer start.""" -# pylint: disable=missing-type-doc - - -# 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, client=None): - """Initialize a new instance of the framer. - - :param decoder: The decoder implementation to use - """ - self.decoder = decoder - self.client = client - - def _validate_unit_id(self, units, single): - """Validate if the received data is valid for the client. - - :param units: list of unit 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 units or 0xFF in units: - # Handle Modbus TCP unit identifier (0x00 0r 0xFF) - # in asynchronous requests - return True - return self._header["uid"] in units # pylint: disable=no-member - - def sendPacket(self, message): - """Send packets on the bus. - - With 3.5char delay between frames - :param message: Message to be sent over the bus - :return: - """ - return self.client.send(message) - - def recvPacket(self, size): - """Receive packet from the bus. - - With specified len - :param size: Number of bytes to read - :return: - """ - return self.client.recv(size) +"""Framer""" + +__all__ = [ + "ModbusFramer", + "ModbusAsciiFramer", + "ModbusBinaryFramer", + "ModbusRtuFramer", + "ModbusSocketFramer", + "ModbusTlsFramer", +] + +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 diff --git a/pymodbus/framer/ascii_framer.py b/pymodbus/framer/ascii_framer.py index 12e8b0e85..ec6a6e057 100644 --- a/pymodbus/framer/ascii_framer.py +++ b/pymodbus/framer/ascii_framer.py @@ -4,7 +4,7 @@ from binascii import a2b_hex, b2a_hex from pymodbus.exceptions import ModbusIOException -from pymodbus.framer import BYTE_ORDER, FRAME_HEADER, ModbusFramer +from pymodbus.framer.base import BYTE_ORDER, FRAME_HEADER, ModbusFramer from pymodbus.logging import Log from pymodbus.utilities import checkLRC, computeLRC @@ -52,7 +52,7 @@ def decode_data(self, data): if len(data) > 1: uid = int(data[1:3], 16) fcode = int(data[3:5], 16) - return {"unit": uid, "fcode": fcode} + return {"slave": uid, "fcode": fcode} return {} def checkFrame(self): @@ -137,12 +137,12 @@ def populateResult(self, result): :param result: The response packet """ - result.unit_id = self._header["uid"] + result.slave_id = self._header["uid"] # ----------------------------------------------------------------------- # # Public Member Functions # ----------------------------------------------------------------------- # - def processIncomingPacket(self, data, callback, unit, **kwargs): + def processIncomingPacket(self, data, callback, slave, **kwargs): """Process new packet pattern. This takes in a new request packet, adds it to the current @@ -156,30 +156,30 @@ def processIncomingPacket(self, data, callback, unit, **kwargs): :param data: The new packet data :param callback: The function to send results to - :param unit: Process if unit id matches, ignore otherwise (could be a - list of unit ids (server) or single unit id(client/server)) + :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: """ - if not isinstance(unit, (list, tuple)): - unit = [unit] + if not isinstance(slave, (list, tuple)): + slave = [slave] single = kwargs.get("single", False) self.addToFrame(data) while self.isFrameReady(): - if self.checkFrame(): - if self._validate_unit_id(unit, single): - frame = self.getFrame() - if (result := self.decoder.decode(frame)) is None: - raise ModbusIOException("Unable to decode response") - self.populateResult(result) - self.advanceFrame() - callback(result) # defer this - else: - header_txt = self._header["uid"] - Log.error("Not a valid unit id - {}, ignoring!!", header_txt) - self.resetFrame() - else: + if not self.checkFrame(): break + if not self._validate_slave_id(slave, single): + header_txt = self._header["uid"] + Log.error("Not a valid slave id - {}, ignoring!!", header_txt) + self.resetFrame() + continue + + frame = self.getFrame() + if (result := self.decoder.decode(frame)) is None: + raise ModbusIOException("Unable to decode response") + self.populateResult(result) + self.advanceFrame() + callback(result) # defer this def buildPacket(self, message): """Create a ready to send modbus packet. @@ -190,19 +190,16 @@ def buildPacket(self, message): :return: The encoded packet """ encoded = message.encode() - buffer = struct.pack(ASCII_FRAME_HEADER, message.unit_id, message.function_code) + buffer = struct.pack( + ASCII_FRAME_HEADER, message.slave_id, message.function_code + ) checksum = computeLRC(encoded + buffer) packet = bytearray() - params = (message.unit_id, message.function_code) packet.extend(self._start) - packet.extend( - ("%02x%02x" % params).encode() # pylint: disable=consider-using-f-string - ) + packet.extend(f"{message.slave_id:02x}{message.function_code:02x}".encode()) packet.extend(b2a_hex(encoded)) - packet.extend( - ("%02x" % checksum).encode() # pylint: disable=consider-using-f-string - ) + packet.extend(f"{checksum:02x}".encode()) packet.extend(self._end) return bytes(packet).upper() diff --git a/pymodbus/framer/base.py b/pymodbus/framer/base.py new file mode 100644 index 000000000..63f081939 --- /dev/null +++ b/pymodbus/framer/base.py @@ -0,0 +1,68 @@ +"""Framer start.""" +# pylint: disable=missing-type-doc +from typing import Any, Dict, Union + +from pymodbus.factory import ClientDecoder, ServerDecoder + + +# 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: Union[ClientDecoder, ServerDecoder], + client=None, + ) -> 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] = {} + + def _validate_slave_id(self, slaves: list, single: bool) -> bool: + """Validate if the received data is valid for the client. + + :param slaves: list of slave id for which the transaction is valid + :param single: Set to true to treat this as a single context + :return: + """ + if single: + return True + if 0 in slaves or 0xFF in slaves: + # Handle Modbus TCP slave identifier (0x00 0r 0xFF) + # in asynchronous requests + return True + return self._header["uid"] in slaves + + def sendPacket(self, message): + """Send packets on the bus. + + With 3.5char delay between frames + :param message: Message to be sent over the bus + :return: + """ + return self.client.send(message) + + def recvPacket(self, size): + """Receive packet from the bus. + + With specified len + :param size: Number of bytes to read + :return: + """ + return self.client.recv(size) diff --git a/pymodbus/framer/binary_framer.py b/pymodbus/framer/binary_framer.py index 37d5b21cf..d065d9fb4 100644 --- a/pymodbus/framer/binary_framer.py +++ b/pymodbus/framer/binary_framer.py @@ -3,7 +3,7 @@ import struct from pymodbus.exceptions import ModbusIOException -from pymodbus.framer import BYTE_ORDER, FRAME_HEADER, ModbusFramer +from pymodbus.framer.base import BYTE_ORDER, FRAME_HEADER, ModbusFramer from pymodbus.logging import Log from pymodbus.utilities import checkCRC, computeCRC @@ -62,10 +62,10 @@ def decode_data(self, data): if len(data) > self._hsize: uid = struct.unpack(">B", data[1:2])[0] fcode = struct.unpack(">B", data[2:3])[0] - return {"unit": uid, "fcode": fcode} + return {"slave": uid, "fcode": fcode} return {} - def checkFrame(self): + def checkFrame(self) -> bool: """Check and decode the next frame. :returns: True if we are successful, False otherwise @@ -84,7 +84,7 @@ def checkFrame(self): return checkCRC(data, self._header["crc"]) return False - def advanceFrame(self): + def advanceFrame(self) -> None: """Skip over the current framed message. This allows us to skip over the current message after we have processed @@ -94,7 +94,7 @@ def advanceFrame(self): self._buffer = self._buffer[self._header["len"] + 2 :] self._header = {"crc": 0x0000, "len": 0, "uid": 0x00} - def isFrameReady(self): + def isFrameReady(self) -> bool: """Check if we should continue decode logic. This is meant to be used in a while loop in the decoding phase to let @@ -134,12 +134,12 @@ def populateResult(self, result): :param result: The response packet """ - result.unit_id = self._header["uid"] + result.slave_id = self._header["uid"] # ----------------------------------------------------------------------- # # Public Member Functions # ----------------------------------------------------------------------- # - def processIncomingPacket(self, data, callback, unit, **kwargs): + def processIncomingPacket(self, data, callback, slave, **kwargs): """Process new packet pattern. This takes in a new request packet, adds it to the current @@ -153,33 +153,30 @@ def processIncomingPacket(self, data, callback, unit, **kwargs): :param data: The new packet data :param callback: The function to send results to - :param unit: Process if unit id matches, ignore otherwise (could be a - list of unit ids (server) or single unit id(client/server) + :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: """ self.addToFrame(data) - if not isinstance(unit, (list, tuple)): - unit = [unit] + if not isinstance(slave, (list, tuple)): + slave = [slave] single = kwargs.get("single", False) while self.isFrameReady(): - if self.checkFrame(): - if self._validate_unit_id(unit, single): - if (result := self.decoder.decode(self.getFrame())) is None: - raise ModbusIOException("Unable to decode response") - self.populateResult(result) - self.advanceFrame() - callback(result) # defer or push to a thread? - else: - header_txt = self._header["uid"] - Log.debug("Not a valid unit id - {}, ignoring!!", header_txt) - self.resetFrame() - break - - else: + if not self.checkFrame(): Log.debug("Frame check failed, ignoring!!") self.resetFrame() 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 + if (result := self.decoder.decode(self.getFrame())) is None: + raise ModbusIOException("Unable to decode response") + self.populateResult(result) + self.advanceFrame() + callback(result) # defer or push to a thread? def buildPacket(self, message): """Create a ready to send modbus packet. @@ -189,7 +186,7 @@ def buildPacket(self, message): """ data = self._preflight(message.encode()) packet = ( - struct.pack(BINARY_FRAME_HEADER, message.unit_id, message.function_code) + struct.pack(BINARY_FRAME_HEADER, message.slave_id, message.function_code) + data ) packet += struct.pack(">H", computeCRC(packet)) diff --git a/pymodbus/framer/rtu_framer.py b/pymodbus/framer/rtu_framer.py index 3fbc54091..b24cfb3ef 100644 --- a/pymodbus/framer/rtu_framer.py +++ b/pymodbus/framer/rtu_framer.py @@ -7,7 +7,7 @@ InvalidMessageReceivedException, ModbusIOException, ) -from pymodbus.framer import BYTE_ORDER, FRAME_HEADER, ModbusFramer +from pymodbus.framer.base import BYTE_ORDER, FRAME_HEADER, ModbusFramer from pymodbus.logging import Log from pymodbus.utilities import ModbusTransactionState, checkCRC, computeCRC @@ -64,6 +64,7 @@ def __init__(self, decoder, client=None): self._hsize = 0x01 self._end = b"\x0d\x0a" self._min_frame_size = 4 + self.function_codes = set(self.decoder.lookup) if self.decoder else {} # ----------------------------------------------------------------------- # # Private Helper Functions @@ -73,7 +74,7 @@ def decode_data(self, data): if len(data) > self._hsize: uid = int(data[0]) fcode = int(data[1]) - return {"unit": uid, "fcode": fcode} + return {"slave": uid, "fcode": fcode} return {} def checkFrame(self): @@ -117,7 +118,7 @@ def resetFrame(self): Log.debug( "Resetting frame - Current Frame in buffer - {}", self._buffer, ":hex" ) - self._buffer = b"" + # self._buffer = b"" self._header = {"uid": 0x00, "len": 0, "crc": b"\x00\x00"} def isFrameReady(self): @@ -188,13 +189,30 @@ def populateResult(self, result): :param result: The response packet """ - result.unit_id = self._header["uid"] + result.slave_id = self._header["uid"] result.transaction_id = self._header["uid"] + def getFrameStart(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: + continue + if i: + self._buffer = self._buffer[i:] # remove preceding trash. + return True + if buf_len > 3: + self._buffer = self._buffer[-3:] + return False + # ----------------------------------------------------------------------- # # Public Member Functions # ----------------------------------------------------------------------- # - def processIncomingPacket(self, data, callback, unit, **kwargs): + def processIncomingPacket(self, data, callback, slave, **kwargs): """Process new packet pattern. This takes in a new request packet, adds it to the current @@ -208,31 +226,32 @@ def processIncomingPacket(self, data, callback, unit, **kwargs): :param data: The new packet data :param callback: The function to send results to - :param unit: Process if unit id matches, ignore otherwise (could be a - list of unit ids (server) or single unit id(client/server) + :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: """ - if not isinstance(unit, (list, tuple)): - unit = [unit] + if not isinstance(slave, (list, tuple)): + slave = [slave] + broadcast = not slave[0] self.addToFrame(data) single = kwargs.get("single", False) - while True: - if self.isFrameReady(): - if self.checkFrame(): - if self._validate_unit_id(unit, single): - self._process(callback) - else: - header_txt = self._header["uid"] - Log.debug("Not a valid unit id - {}, ignoring!!", header_txt) - self.resetFrame() - break - else: - Log.debug("Frame check failed, ignoring!!") - self.resetFrame() - break - else: + skip_cur_frame = False + while self.getFrameStart(slave, broadcast, skip_cur_frame): + if not self.isFrameReady(): Log.debug("Frame - [{}] not ready", data) break + if not self.checkFrame(): + Log.debug("Frame check failed, ignoring!!") + self.resetFrame() + skip_cur_frame = True + continue + 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() + skip_cur_frame = True + continue + self._process(callback) def buildPacket(self, message): """Create a ready to send modbus packet. @@ -241,11 +260,12 @@ def buildPacket(self, message): """ data = message.encode() packet = ( - struct.pack(RTU_FRAME_HEADER, message.unit_id, message.function_code) + data + struct.pack(RTU_FRAME_HEADER, message.slave_id, message.function_code) + + data ) packet += struct.pack(">H", computeCRC(packet)) - # Ensure that transaction is actually the unit id for serial comms - message.transaction_id = message.unit_id + # Ensure that transaction is actually the slave id for serial comms + message.transaction_id = message.slave_id return packet def sendPacket(self, message): diff --git a/pymodbus/framer/socket_framer.py b/pymodbus/framer/socket_framer.py index 58414c037..043d2b0d6 100644 --- a/pymodbus/framer/socket_framer.py +++ b/pymodbus/framer/socket_framer.py @@ -6,7 +6,7 @@ InvalidMessageReceivedException, ModbusIOException, ) -from pymodbus.framer import SOCKET_FRAME_HEADER, ModbusFramer +from pymodbus.framer.base import SOCKET_FRAME_HEADER, ModbusFramer from pymodbus.logging import Log @@ -117,7 +117,7 @@ def populateResult(self, result): """ result.transaction_id = self._header["tid"] result.protocol_id = self._header["pid"] - result.unit_id = self._header["uid"] + result.slave_id = self._header["uid"] # ----------------------------------------------------------------------- # # Public Member Functions @@ -132,12 +132,12 @@ def decode_data(self, data): "tid": tid, "pid": pid, "length": length, - "unit": uid, + "slave": uid, "fcode": fcode, } return {} - def processIncomingPacket(self, data, callback, unit, **kwargs): + def processIncomingPacket(self, data, callback, slave, tid: int = None, **kwargs): """Process new packet pattern. This takes in a new request packet, adds it to the current @@ -148,47 +148,44 @@ def processIncomingPacket(self, data, callback, unit, **kwargs): 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 unit: Process if unit id matches, ignore otherwise (could be a - list of unit ids (server) or single unit id(client/server) - :param kwargs: """ - if not isinstance(unit, (list, tuple)): - unit = [unit] + if not isinstance(slave, (list, tuple)): + slave = [slave] single = kwargs.get("single", False) Log.debug("Processing: {}", data, ":hex") self.addToFrame(data) while True: - if self.isFrameReady(): - if self.checkFrame(): - if self._validate_unit_id(unit, single): - self._process(callback) - else: - header_txt = self._header["uid"] - Log.debug("Not a valid unit id - {}, ignoring!!", header_txt) - self.resetFrame() - else: - Log.debug("Frame check failed, ignoring!!") - self.resetFrame() - else: + if not self.isFrameReady(): if len(self._buffer): # Possible error ??? if self._header["len"] < 2: - self._process(callback, error=True) + self._process(callback, tid, error=True) break - - def _process(self, callback, error=False): + if not self.checkFrame(): + Log.debug("Frame check failed, ignoring!!") + self.resetFrame() + continue + 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() + continue + self._process(callback, tid) + + def _process(self, callback, tid, error=False): """Process incoming packets irrespective error condition.""" data = self.getRawFrame() if error else self.getFrame() if (result := self.decoder.decode(data)) is None: + self.resetFrame() raise ModbusIOException("Unable to decode request") if error and result.function_code < 0x80: raise InvalidMessageReceivedException(result) self.populateResult(result) self.advanceFrame() - callback(result) # defer or push to a thread? + if tid and tid != result.transaction_id: + self.resetFrame() + else: + callback(result) # defer or push to a thread? def resetFrame(self): """Reset the entire message frame. @@ -217,7 +214,7 @@ def buildPacket(self, message): message.transaction_id, message.protocol_id, len(data) + 2, - message.unit_id, + message.slave_id, message.function_code, ) packet += data diff --git a/pymodbus/framer/tls_framer.py b/pymodbus/framer/tls_framer.py index f1d582656..252bedb0b 100644 --- a/pymodbus/framer/tls_framer.py +++ b/pymodbus/framer/tls_framer.py @@ -6,7 +6,7 @@ InvalidMessageReceivedException, ModbusIOException, ) -from pymodbus.framer import TLS_FRAME_HEADER, ModbusFramer +from pymodbus.framer.base import TLS_FRAME_HEADER, ModbusFramer from pymodbus.logging import Log @@ -101,7 +101,7 @@ def decode_data(self, data): return {"fcode": fcode} return {} - def processIncomingPacket(self, data, callback, unit, **kwargs): + def processIncomingPacket(self, data, callback, slave, **kwargs): """Process new packet pattern. This takes in a new request packet, adds it to the current @@ -115,27 +115,27 @@ def processIncomingPacket(self, data, callback, unit, **kwargs): :param data: The new packet data :param callback: The function to send results to - :param unit: Process if unit id matches, ignore otherwise (could be a - list of unit ids (server) or single unit id(client/server) + :param slave: Process if slave id matcheks, ignore otherwise (could be a + list of slave ids (server) or single slave id(client/server) :param kwargs: """ - if not isinstance(unit, (list, tuple)): - unit = [unit] - # no unit id for Modbus Security Application Protocol + if not isinstance(slave, (list, tuple)): + slave = [slave] + # no slave id for Modbus Security Application Protocol single = kwargs.get("single", True) Log.debug("Processing: {}", data, ":hex") self.addToFrame(data) - if self.isFrameReady(): - if self.checkFrame(): - if self._validate_unit_id(unit, single): - self._process(callback) - else: - Log.debug("Not in valid unit id - {}, ignoring!!", unit) - self.resetFrame() - else: - Log.debug("Frame check failed, ignoring!!") - self.resetFrame() + if not self.isFrameReady(): + return + if not self.checkFrame(): + Log.debug("Frame check failed, ignoring!!") + self.resetFrame() + return + if not self._validate_slave_id(slave, single): + Log.debug("Not in valid slave id - {}, ignoring!!", slave) + self.resetFrame() + self._process(callback) def _process(self, callback, error=False): """Process incoming packets irrespective error condition.""" diff --git a/pymodbus/logging.py b/pymodbus/logging.py index f49e8ed39..61492e616 100644 --- a/pymodbus/logging.py +++ b/pymodbus/logging.py @@ -26,6 +26,8 @@ def pymodbus_apply_logging_config( Please call this function to format logging appropriately when opening issues. """ + if isinstance(level, str): + level = level.upper() Log.apply_logging_config(level, log_file_name) @@ -42,6 +44,8 @@ def apply_logging_config(cls, level, log_file_name): """Apply basic logging configuration""" if level == logging.NOTSET: level = cls._logger.getEffectiveLevel() + if isinstance(level, str): + level = level.upper() log_stream_handler = logging.StreamHandler() log_formatter = logging.Formatter( "%(asctime)s %(levelname)-5s %(module)s:%(lineno)s %(message)s" diff --git a/pymodbus/mei_message.py b/pymodbus/mei_message.py index 0b44fb97b..5a1a6b1db 100644 --- a/pymodbus/mei_message.py +++ b/pymodbus/mei_message.py @@ -1,4 +1,10 @@ """Encapsulated Interface (MEI) Transport Messages.""" + +__all__ = [ + "ReadDeviceInformationRequest", + "ReadDeviceInformationResponse", +] + # pylint: disable=missing-type-doc import struct @@ -164,7 +170,7 @@ def encode(self): self.space_left = 253 - 6 objects = b"" try: - for (object_id, data) in iter(self.information.items()): + for object_id, data in iter(self.information.items()): if isinstance(data, list): for item in data: objects += self._encode_object(object_id, item) @@ -210,12 +216,3 @@ def __str__(self): :returns: The string representation of the response """ return f"ReadDeviceInformationResponse({self.read_code})" - - -# ---------------------------------------------------------------------------# -# Exported symbols -# ---------------------------------------------------------------------------# -__all__ = [ - "ReadDeviceInformationRequest", - "ReadDeviceInformationResponse", -] diff --git a/pymodbus/other_message.py b/pymodbus/other_message.py index e67d2d35c..ae96950c0 100644 --- a/pymodbus/other_message.py +++ b/pymodbus/other_message.py @@ -2,6 +2,18 @@ Currently not all implemented """ + +__all__ = [ + "ReadExceptionStatusRequest", + "ReadExceptionStatusResponse", + "GetCommEventCounterRequest", + "GetCommEventCounterResponse", + "GetCommEventLogRequest", + "GetCommEventLogResponse", + "ReportSlaveIdRequest", + "ReportSlaveIdResponse", +] + # pylint: disable=missing-type-doc import struct @@ -28,9 +40,9 @@ class ReadExceptionStatusRequest(ModbusRequest): function_code_name = "read_exception_status" _rtu_frame_size = 4 - def __init__(self, unit=None, **kwargs): + def __init__(self, slave=None, **kwargs): """Initialize a new instance.""" - ModbusRequest.__init__(self, unit=unit, **kwargs) + ModbusRequest.__init__(self, slave=slave, **kwargs) def encode(self): """Encode the message.""" @@ -365,13 +377,13 @@ class ReportSlaveIdRequest(ModbusRequest): function_code_name = "report_slave_id" _rtu_frame_size = 4 - def __init__(self, unit=Defaults.Slave, **kwargs): + def __init__(self, slave=Defaults.Slave, **kwargs): """Initialize a new instance. - :param unit: Modbus slave unit ID + :param slave: Modbus slave slave ID """ - ModbusRequest.__init__(self, unit, **kwargs) + ModbusRequest.__init__(self, slave, **kwargs) def encode(self): """Encode the message.""" @@ -463,34 +475,9 @@ def decode(self, data): status = int(data[-1]) self.status = status == ModbusStatus.SlaveOn - def __str__(self): + def __str__(self) -> str: """Build a representation of the response. :returns: The string representation of the response """ - arguments = (self.function_code, self.identifier, self.status) - return ( - "ReportSlaveIdResponse(%s, %s, %s)" # pylint: disable=consider-using-f-string - % arguments - ) - - -# ---------------------------------------------------------------------------# -# TODO Make these only work on serial # pylint: disable=fixme -# ---------------------------------------------------------------------------# -# report device identification 43, 14 - - -# ---------------------------------------------------------------------------# -# Exported symbols -# ---------------------------------------------------------------------------# -__all__ = [ - "ReadExceptionStatusRequest", - "ReadExceptionStatusResponse", - "GetCommEventCounterRequest", - "GetCommEventCounterResponse", - "GetCommEventLogRequest", - "GetCommEventLogResponse", - "ReportSlaveIdRequest", - "ReportSlaveIdResponse", -] + return f"ReportSlaveIdResponse({self.function_code}, {self.identifier}, {self.status})" diff --git a/pymodbus/payload.py b/pymodbus/payload.py index a5de2dc12..e815fa951 100644 --- a/pymodbus/payload.py +++ b/pymodbus/payload.py @@ -3,14 +3,20 @@ A collection of utilities for building and decoding modbus messages payloads. """ + +__all__ = [ + "BinaryPayloadBuilder", + "BinaryPayloadDecoder", +] + # pylint: disable=missing-type-doc from struct import pack, unpack +from typing import List from pymodbus.constants import Endian from pymodbus.exceptions import ParameterException from pymodbus.logging import Log from pymodbus.utilities import ( - make_byte_string, pack_bitstring, unpack_bitstring, ) @@ -61,9 +67,7 @@ def _pack_words(self, fstring, value): :param value: Value to be packed :return: """ - value = pack( - "!{}".format(fstring), value # pylint: disable=consider-using-f-string - ) + value = pack(f"!{fstring}", value) wordorder = WC.get(fstring.lower()) // 2 upperbyte = f"!{wordorder}H" payload = unpack(upperbyte, value) @@ -77,21 +81,18 @@ def _pack_words(self, fstring, value): return payload - def to_string(self): - """Return the payload buffer as a string. - - :returns: The payload buffer as a string - """ + def encode(self) -> bytes: + """Get the payload buffer encoded in bytes.""" return b"".join(self._payload) - def __str__(self): + def __str__(self) -> str: """Return the payload buffer as a string. :returns: The payload buffer as a string """ - return self.to_string().decode("utf-8") + return self.encode().decode("utf-8") - def reset(self): + def reset(self) -> None: """Reset the payload buffer.""" self._payload = [] @@ -110,7 +111,7 @@ def to_registers(self): Log.debug("{}", payload) return payload - def to_coils(self): + def to_coils(self) -> List[bool]: """Convert the payload buffer into a coil layout that can be used as a context block. :returns: The coil layout to use as a block @@ -119,7 +120,7 @@ def to_coils(self): coils = [bool(int(bit)) for reg in payload for bit in format(reg, "016b")] return coils - def build(self): + def build(self) -> List[bytes]: """Return the payload buffer as a list. This list is two bytes per element and can @@ -127,12 +128,12 @@ def build(self): :returns: The payload buffer as a list """ - string = self.to_string() - length = len(string) - string = string + (b"\x00" * (length % 2)) - return [string[i : i + 2] for i in range(0, length, 2)] + buffer = self.encode() + length = len(buffer) + buffer += b"\x00" * (length % 2) + return [buffer[i : i + 2] for i in range(0, length, 2)] - def add_bits(self, values): + def add_bits(self, values: List[bool]) -> None: """Add a collection of bits to be encoded. If these are less than a multiple of eight, @@ -144,7 +145,7 @@ def add_bits(self, values): value = pack_bitstring(values) self._payload.append(value) - def add_8bit_uint(self, value): + def add_8bit_uint(self, value: int) -> None: """Add a 8 bit unsigned int to the buffer. :param value: The value to add to the buffer @@ -152,7 +153,7 @@ def add_8bit_uint(self, value): fstring = self._byteorder + "B" self._payload.append(pack(fstring, value)) - def add_16bit_uint(self, value): + def add_16bit_uint(self, value: int) -> None: """Add a 16 bit unsigned int to the buffer. :param value: The value to add to the buffer @@ -160,7 +161,7 @@ def add_16bit_uint(self, value): fstring = self._byteorder + "H" self._payload.append(pack(fstring, value)) - def add_32bit_uint(self, value): + def add_32bit_uint(self, value: int) -> None: """Add a 32 bit unsigned int to the buffer. :param value: The value to add to the buffer @@ -170,7 +171,7 @@ def add_32bit_uint(self, value): p_string = self._pack_words(fstring, value) self._payload.append(p_string) - def add_64bit_uint(self, value): + def add_64bit_uint(self, value: int) -> None: """Add a 64 bit unsigned int to the buffer. :param value: The value to add to the buffer @@ -179,7 +180,7 @@ def add_64bit_uint(self, value): p_string = self._pack_words(fstring, value) self._payload.append(p_string) - def add_8bit_int(self, value): + def add_8bit_int(self, value: int) -> None: """Add a 8 bit signed int to the buffer. :param value: The value to add to the buffer @@ -187,7 +188,7 @@ def add_8bit_int(self, value): fstring = self._byteorder + "b" self._payload.append(pack(fstring, value)) - def add_16bit_int(self, value): + def add_16bit_int(self, value: int) -> None: """Add a 16 bit signed int to the buffer. :param value: The value to add to the buffer @@ -195,7 +196,7 @@ def add_16bit_int(self, value): fstring = self._byteorder + "h" self._payload.append(pack(fstring, value)) - def add_32bit_int(self, value): + def add_32bit_int(self, value: int) -> None: """Add a 32 bit signed int to the buffer. :param value: The value to add to the buffer @@ -204,7 +205,7 @@ def add_32bit_int(self, value): p_string = self._pack_words(fstring, value) self._payload.append(p_string) - def add_64bit_int(self, value): + def add_64bit_int(self, value: int) -> None: """Add a 64 bit signed int to the buffer. :param value: The value to add to the buffer @@ -213,7 +214,7 @@ def add_64bit_int(self, value): p_string = self._pack_words(fstring, value) self._payload.append(p_string) - def add_16bit_float(self, value): + def add_16bit_float(self, value: float) -> None: """Add a 16 bit float to the buffer. :param value: The value to add to the buffer @@ -222,7 +223,7 @@ def add_16bit_float(self, value): p_string = self._pack_words(fstring, value) self._payload.append(p_string) - def add_32bit_float(self, value): + def add_32bit_float(self, value: float) -> None: """Add a 32 bit float to the buffer. :param value: The value to add to the buffer @@ -231,7 +232,7 @@ def add_32bit_float(self, value): p_string = self._pack_words(fstring, value) self._payload.append(p_string) - def add_64bit_float(self, value): + def add_64bit_float(self, value: float) -> None: """Add a 64 bit float(double) to the buffer. :param value: The value to add to the buffer @@ -240,14 +241,13 @@ def add_64bit_float(self, value): p_string = self._pack_words(fstring, value) self._payload.append(p_string) - def add_string(self, value): + def add_string(self, value: str) -> None: """Add a string to the buffer. :param value: The value to add to the buffer """ - value = make_byte_string(value) fstring = self._byteorder + str(len(value)) + "s" - self._payload.append(pack(fstring, value)) + self._payload.append(pack(fstring, value.encode())) class BinaryPayloadDecoder: @@ -338,7 +338,6 @@ def _unpack_words(self, fstring, handle): :param handle: Value to be unpacked :return: """ - handle = make_byte_string(handle) wc_value = WC.get(fstring.lower()) // 2 handle = unpack(f"!{wc_value}H", handle) if self._wordorder == Endian.Little: @@ -359,7 +358,6 @@ def decode_8bit_uint(self): self._pointer += 1 fstring = self._byteorder + "B" handle = self._payload[self._pointer - 1 : self._pointer] - handle = make_byte_string(handle) return unpack(fstring, handle)[0] def decode_bits(self, package_len=1): @@ -367,7 +365,6 @@ def decode_bits(self, package_len=1): self._pointer += package_len # fstring = self._endian + "B" handle = self._payload[self._pointer - 1 : self._pointer] - handle = make_byte_string(handle) return unpack_bitstring(handle) def decode_16bit_uint(self): @@ -375,14 +372,12 @@ def decode_16bit_uint(self): self._pointer += 2 fstring = self._byteorder + "H" handle = self._payload[self._pointer - 2 : self._pointer] - handle = make_byte_string(handle) return unpack(fstring, handle)[0] def decode_32bit_uint(self): """Decode a 32 bit unsigned int from the buffer.""" self._pointer += 4 fstring = "I" - # fstring = "I" handle = self._payload[self._pointer - 4 : self._pointer] handle = self._unpack_words(fstring, handle) return unpack("!" + fstring, handle)[0] @@ -400,7 +395,6 @@ def decode_8bit_int(self): self._pointer += 1 fstring = self._byteorder + "b" handle = self._payload[self._pointer - 1 : self._pointer] - handle = make_byte_string(handle) return unpack(fstring, handle)[0] def decode_16bit_int(self): @@ -408,7 +402,6 @@ def decode_16bit_int(self): self._pointer += 2 fstring = self._byteorder + "h" handle = self._payload[self._pointer - 2 : self._pointer] - handle = make_byte_string(handle) return unpack(fstring, handle)[0] def decode_32bit_int(self): @@ -465,9 +458,3 @@ def skip_bytes(self, nbytes): :param nbytes: The number of bytes to skip """ self._pointer += nbytes - - -# ---------------------------------------------------------------------------# -# Exported Identifiers -# ---------------------------------------------------------------------------# -__all__ = ["BinaryPayloadBuilder", "BinaryPayloadDecoder"] diff --git a/pymodbus/pdu.py b/pymodbus/pdu.py index cf151f9c9..ad408c83f 100644 --- a/pymodbus/pdu.py +++ b/pymodbus/pdu.py @@ -1,4 +1,13 @@ """Contains base classes for modbus request/response/error packets.""" + +__all__ = [ + "ModbusRequest", + "ModbusResponse", + "ModbusExceptions", + "ExceptionResponse", + "IllegalFunctionRequest", +] + # pylint: disable=missing-type-doc import struct @@ -24,7 +33,7 @@ class ModbusPDU: This is a constant set at 0 to indicate Modbus. It is put here for ease of expansion. - .. attribute:: unit + .. attribute:: slave_id This is used to route the request to the correct child. In the TCP modbus, it is used for routing (or not used at all. However, @@ -45,15 +54,15 @@ class ModbusPDU: of encoding it again. """ - def __init__(self, unit=Defaults.Slave, **kwargs): + def __init__(self, slave=Defaults.Slave, **kwargs): """Initialize the base data for a modbus request. - :param unit: Modbus slave unit ID + :param slave: Modbus slave slave ID """ self.transaction_id = kwargs.get("transaction", Defaults.TransactionId) self.protocol_id = kwargs.get("protocol", Defaults.ProtocolId) - self.unit_id = unit + self.slave_id = slave self.skip_encode = kwargs.get("skip_encode", False) self.check = 0x0000 @@ -94,12 +103,12 @@ class ModbusRequest(ModbusPDU): function_code = -1 - def __init__(self, unit=Defaults.Slave, **kwargs): + def __init__(self, slave=Defaults.Slave, **kwargs): """Proxy to the lower level initializer. - :param unit: Modbus slave unit ID + :param slave: Modbus slave slave ID """ - super().__init__(unit, **kwargs) + super().__init__(slave, **kwargs) def doException(self, exception): """Build an error response based on the function. @@ -128,13 +137,15 @@ class ModbusResponse(ModbusPDU): should_respond = True - def __init__(self, unit=Defaults.Slave, **kwargs): + def __init__(self, slave=Defaults.Slave, **kwargs): """Proxy the lower level initializer. - :param unit: Modbus slave unit ID + :param slave: Modbus slave slave ID """ - super().__init__(unit, **kwargs) + super().__init__(slave, **kwargs) + self.bits = [] + self.registers = [] def isError(self): """Check if the error is a success or failure.""" @@ -243,17 +254,3 @@ def execute(self, _context): :returns: The error response packet """ return ExceptionResponse(self.function_code, self.ErrorCode) - - -# --------------------------------------------------------------------------- # -# Exported symbols -# --------------------------------------------------------------------------- # - - -__all__ = [ - "ModbusRequest", - "ModbusResponse", - "ModbusExceptions", - "ExceptionResponse", - "IllegalFunctionRequest", -] diff --git a/pymodbus/register_read_message.py b/pymodbus/register_read_message.py index 08a420f0f..5ee0fafc5 100644 --- a/pymodbus/register_read_message.py +++ b/pymodbus/register_read_message.py @@ -1,4 +1,15 @@ """Register Reading Request/Response.""" + +__all__ = [ + "ReadHoldingRegistersRequest", + "ReadHoldingRegistersResponse", + "ReadInputRegistersRequest", + "ReadInputRegistersResponse", + "ReadRegistersResponseBase", + "ReadWriteMultipleRegistersRequest", + "ReadWriteMultipleRegistersResponse", +] + # pylint: disable=missing-type-doc import struct @@ -12,14 +23,14 @@ class ReadRegistersRequestBase(ModbusRequest): _rtu_frame_size = 8 - def __init__(self, address, count, unit=Defaults.Slave, **kwargs): + def __init__(self, address, count, slave=Defaults.Slave, **kwargs): """Initialize a new instance. :param address: The address to start the read from :param count: The number of registers to read - :param unit: Modbus slave unit ID + :param slave: Modbus slave slave ID """ - super().__init__(unit, **kwargs) + super().__init__(slave, **kwargs) self.address = address self.count = count @@ -60,13 +71,13 @@ class ReadRegistersResponseBase(ModbusResponse): _rtu_byte_count_pos = 2 - def __init__(self, values, unit=Defaults.Slave, **kwargs): + def __init__(self, values, slave=Defaults.Slave, **kwargs): """Initialize a new instance. :param values: The values to write to - :param unit: Modbus slave unit ID + :param slave: Modbus slave slave ID """ - super().__init__(unit, **kwargs) + super().__init__(slave, **kwargs) #: A list of register values self.registers = values or [] @@ -120,14 +131,14 @@ class ReadHoldingRegistersRequest(ReadRegistersRequestBase): function_code = 3 function_code_name = "read_holding_registers" - def __init__(self, address=None, count=None, unit=Defaults.Slave, **kwargs): + def __init__(self, address=None, count=None, slave=Defaults.Slave, **kwargs): """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 unit: Modbus slave unit ID + :param slave: Modbus slave slave ID """ - super().__init__(address, count, unit, **kwargs) + super().__init__(address, count, slave, **kwargs) def execute(self, context): """Run a read holding request against a datastore. @@ -178,14 +189,14 @@ class ReadInputRegistersRequest(ReadRegistersRequestBase): function_code = 4 function_code_name = "read_input_registers" - def __init__(self, address=None, count=None, unit=Defaults.Slave, **kwargs): + def __init__(self, address=None, count=None, slave=Defaults.Slave, **kwargs): """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 unit: Modbus slave unit ID + :param slave: Modbus slave slave ID """ - super().__init__(address, count, unit, **kwargs) + super().__init__(address, count, slave, **kwargs) def execute(self, context): """Run a read input request against a datastore. @@ -390,17 +401,3 @@ def __str__(self): :returns: A string representation of the instance """ return f"ReadWriteNRegisterResponse ({len(self.registers)})" - - -# ---------------------------------------------------------------------------# -# Exported symbols -# ---------------------------------------------------------------------------# -__all__ = [ - "ReadHoldingRegistersRequest", - "ReadHoldingRegistersResponse", - "ReadInputRegistersRequest", - "ReadInputRegistersResponse", - "ReadRegistersResponseBase", - "ReadWriteMultipleRegistersRequest", - "ReadWriteMultipleRegistersResponse", -] diff --git a/pymodbus/register_write_message.py b/pymodbus/register_write_message.py index d3da14936..30a04264a 100644 --- a/pymodbus/register_write_message.py +++ b/pymodbus/register_write_message.py @@ -1,4 +1,14 @@ """Register Writing Request/Response Messages.""" + +__all__ = [ + "WriteSingleRegisterRequest", + "WriteSingleRegisterResponse", + "WriteMultipleRegistersRequest", + "WriteMultipleRegistersResponse", + "MaskWriteRegisterRequest", + "MaskWriteRegisterResponse", +] + # pylint: disable=missing-type-doc import struct @@ -18,13 +28,13 @@ class WriteSingleRegisterRequest(ModbusRequest): function_code_name = "write_register" _rtu_frame_size = 8 - def __init__(self, address=None, value=None, unit=None, **kwargs): + def __init__(self, address=None, value=None, slave=None, **kwargs): """Initialize a new instance. :param address: The address to start writing add :param value: The values to write """ - super().__init__(unit=unit, **kwargs) + super().__init__(slave=slave, **kwargs) self.address = address self.value = value @@ -148,13 +158,13 @@ class WriteMultipleRegistersRequest(ModbusRequest): _rtu_byte_count_pos = 6 _pdu_length = 5 # func + adress1 + adress2 + outputQuant1 + outputQuant2 - def __init__(self, address=None, values=None, unit=None, **kwargs): + def __init__(self, address=None, values=None, slave=None, **kwargs): """Initialize a new instance. :param address: The address to start writing to :param values: The values to write """ - super().__init__(unit=unit, **kwargs) + super().__init__(slave=slave, **kwargs) self.address = address if values is None: values = [] @@ -359,16 +369,3 @@ def decode(self, data): :param data: The packet data to decode """ self.address, self.and_mask, self.or_mask = struct.unpack(">HHH", data) - - -# ---------------------------------------------------------------------------# -# Exported symbols -# ---------------------------------------------------------------------------# -__all__ = [ - "WriteSingleRegisterRequest", - "WriteSingleRegisterResponse", - "WriteMultipleRegistersRequest", - "WriteMultipleRegistersResponse", - "MaskWriteRegisterRequest", - "MaskWriteRegisterResponse", -] diff --git a/pymodbus/repl/client/README.md b/pymodbus/repl/client/README.md index a11a01198..9cb7485bf 100644 --- a/pymodbus/repl/client/README.md +++ b/pymodbus/repl/client/README.md @@ -282,7 +282,7 @@ null ``` -To Send broadcast requests, use `--broadcast-support` and send requests with unit id as `0`. +To Send broadcast requests, use `--broadcast-support` and send requests with slave id as `0`. `write_coil`, `write_coils`, `write_register`, `write_registers` are supported. ``` @@ -298,12 +298,12 @@ __________ _____ .___ __________ .__ v1.3.0 - [pymodbus, version 3.0.0] ---------------------------------------------------------------------------- -> client.write_registers address=0 values=10,20,30,40 unit=0 +> client.write_registers address=0 values=10,20,30,40 slave=0 { "broadcasted": true } -> client.write_registers address=0 values=10,20,30,40 unit=1 +> client.write_registers address=0 values=10,20,30,40 slave=1 { "address": 0, "count": 4 diff --git a/pymodbus/repl/client/helper.py b/pymodbus/repl/client/helper.py index 7790eb0f0..94ac748a6 100644 --- a/pymodbus/repl/client/helper.py +++ b/pymodbus/repl/client/helper.py @@ -33,7 +33,7 @@ } -DEFAULT_KWARGS = {"unit": "Slave address"} +DEFAULT_KWARGS = {"slave": "Slave address"} OTHER_COMMANDS = { "result.raw": "Show RAW Result", @@ -65,13 +65,13 @@ class Command: """Class representing Commands to be consumed by Completer.""" - def __init__(self, name, signature, doc, unit=False): + def __init__(self, name, signature, doc, slave=False): """Initialize. :param name: Name of the command :param signature: inspect object :param doc: Doc string for the command - :param unit: Use unit as additional argument in the command . + :param slave: Use slave as additional argument in the command . """ self.name = name self.doc = doc.split("\n") if doc else " ".join(name.split("_")) @@ -83,7 +83,7 @@ def __init__(self, name, signature, doc, unit=False): else: self._params = "" - if self.name.startswith("client.") and unit: + if self.name.startswith("client.") and slave: self.args.update(**DEFAULT_KWARGS) def _create_help(self): @@ -165,7 +165,7 @@ def _get_requests(members): ) commands = { f"client.{c[0]}": Command( - f"client.{c[0]}", argspec(c[1]), inspect.getdoc(c[1]), unit=False + f"client.{c[0]}", argspec(c[1]), inspect.getdoc(c[1]), slave=False ) for c in commands if not c[0].startswith("_") @@ -180,7 +180,7 @@ def _get_client_methods(members): ) commands = { "client.{c[0]}": Command( - "client.{c[0]}", argspec(c[1]), inspect.getdoc(c[1]), unit=False + "client.{c[0]}", argspec(c[1]), inspect.getdoc(c[1]), slave=False ) for c in commands if not c[0].startswith("_") @@ -193,7 +193,7 @@ def _get_client_properties(members): global CLIENT_ATTRIBUTES # pylint: disable=global-variable-not-assigned commands = list(filter(lambda x: not callable(x[1]), members)) commands = { - f"client.{c[0]}": Command(f"client.{c[0]}", None, "Read Only!", unit=False) + f"client.{c[0]}": Command(f"client.{c[0]}", None, "Read Only!", slave=False) for c in commands if (not c[0].startswith("_") and isinstance(c[1], (str, int, float))) } diff --git a/pymodbus/repl/client/main.py b/pymodbus/repl/client/main.py index f3b12ba5e..27341607e 100644 --- a/pymodbus/repl/client/main.py +++ b/pymodbus/repl/client/main.py @@ -95,11 +95,7 @@ def convert(self, value, param, ctx): return choice self.fail( - "invalid choice: %s. (choose from %s)" # pylint: disable=consider-using-f-string - % ( - value, - ", ".join(self.choices), - ), + f"invalid choice: {value}. (choose from {', '.join(self.choices)})", param, ctx, ) @@ -151,90 +147,103 @@ def _parse_val(arg_name, val): return kwargs, execute -def cli(client): # pylint: disable=too-complex - """Run client definition.""" - use_keys = KeyBindings() - history_file = pathlib.Path.home().joinpath(".pymodhis") +class CLI: # pylint: disable=too-few-public-methods + """Client definition.""" - @use_keys.add("c-space") - def _(event): - """Initialize autocompletion, or select the next completion.""" - buff = event.app.current_buffer - if buff.complete_state: - buff.complete_next() - else: - buff.start_completion(select_first=False) - - @use_keys.add("enter", filter=has_selected_completion) - def _(event): - """Make the enter key work as the tab key only when showing the menu.""" - event.current_buffer.complete_state = None - buffer = event.cli.current_buffer - buffer.complete_state = None - - session = PromptSession( - lexer=PygmentsLexer(PythonLexer), - completer=CmdCompleter(client), - style=style, - complete_while_typing=True, - bottom_toolbar=bottom_toolbar, - key_bindings=use_keys, - history=FileHistory(history_file), - auto_suggest=AutoSuggestFromHistory(), - ) - click.secho(TITLE, fg="green") - result = None - while True: # pylint: disable=too-many-nested-blocks - try: - - text = session.prompt("> ", complete_while_typing=True) - if text.strip().lower() == "help": - print_formatted_text(HTML("Available commands:")) - for cmd, obj in sorted(session.completer.commands.items()): - if cmd != "help": - print_formatted_text( - HTML( - "{:45s}" # pylint: disable=consider-using-f-string - "{:100s}" - "".format(cmd, obj.help_text) - ) - ) - - continue - if text.strip().lower() == "exit": - raise EOFError() - if text.strip().lower().startswith("client."): - try: - text = text.strip().split() - cmd = text[0].split(".")[1] - args = text[1:] - kwargs, execute = _process_args(args, string=False) - if execute: - if text[0] in CLIENT_ATTRIBUTES: - result = Result(getattr(client, cmd)) - else: - result = Result(getattr(client, cmd)(**kwargs)) - result.print_result() - except Exception as exc: # pylint: disable=broad-except - click.secho(repr(exc), fg="red") - elif text.strip().lower().startswith("result."): - if result: - words = text.lower().split() - if words[0] == "result.raw": - result.raw() - if words[0] == "result.decode": - args = words[1:] - kwargs, execute = _process_args(args) - if execute: - result.decode(**kwargs) - except KeyboardInterrupt: - continue # Control-C pressed. Try again. - except EOFError: - break # Control-D pressed. - except Exception as exc: # pylint: disable=broad-except - click.secho(str(exc), fg="red") - - click.secho("GoodBye!", fg="blue") + def __init__(self, client): + """Set up client and keybindings.""" + + use_keys = KeyBindings() + history_file = pathlib.Path.home().joinpath(".pymodhis") + self.client = client + + @use_keys.add("c-space") + def _(event): + """Initialize autocompletion, or select the next completion.""" + buff = event.app.current_buffer + if buff.complete_state: + buff.complete_next() + else: + buff.start_completion(select_first=False) + + @use_keys.add("enter", filter=has_selected_completion) + def _(event): + """Make the enter key work as the tab key only when showing the menu.""" + event.current_buffer.complete_state = None + buffer = event.cli.current_buffer + buffer.complete_state = None + + self.session = PromptSession( + lexer=PygmentsLexer(PythonLexer), + completer=CmdCompleter(client), + style=style, + complete_while_typing=True, + bottom_toolbar=bottom_toolbar, + key_bindings=use_keys, + history=FileHistory(history_file), + auto_suggest=AutoSuggestFromHistory(), + ) + click.secho(TITLE, fg="green") + + def _print_command_help(self, commands): + """Print a list of commands with help text.""" + for cmd, obj in sorted(commands.items()): + if cmd != "help": + print_formatted_text( + HTML( + "{:45s}" # pylint: disable=consider-using-f-string + "{:100s}" + "".format(cmd, obj.help_text) + ) + ) + + def _process_client(self, text, client): + """Process client commands.""" + text = text.strip().split() + cmd = text[0].split(".")[1] + args = text[1:] + kwargs, execute = _process_args(args, string=False) + if execute: + if text[0] in CLIENT_ATTRIBUTES: + result = Result(getattr(client, cmd)) + else: + result = Result(getattr(client, cmd)(**kwargs)) + result.print_result() + + def _process_result(self, text, result): + """Process result commands.""" + words = text.split() + if words[0] == "result.raw": + result.raw() + if words[0] == "result.decode": + args = words[1:] + kwargs, execute = _process_args(args) + if execute: + result.decode(**kwargs) + + def run(self): + """Run the REPL.""" + result = None + while True: + try: + text = self.session.prompt("> ", complete_while_typing=True) + if text.strip().lower() == "help": + print_formatted_text(HTML("Available commands:")) + self._print_command_help(self.session.completer.commands) + elif text.strip().lower() == "exit": + raise EOFError() + elif text.strip().lower().startswith("client."): + self._process_client(text, self.client) + elif text.strip().lower().startswith("result.") and result: + self._process_result(text, result) + except KeyboardInterrupt: + continue # Control-C pressed. Try again. + except EOFError: + break # Control-D pressed. + except Exception as exc: # pylint: disable=broad-except + click.secho(str(exc), fg="red") + + click.secho("GoodBye!", fg="blue") @click.group("pymodbus-repl") @@ -277,7 +286,7 @@ def main( logging.basicConfig(format=use_format) _logger.setLevel(logging.DEBUG) ctx.obj = { - "broadcast": broadcast_support, + "broadcast_enable": broadcast_support, "retry_on_empty": retry_on_empty, "retry_on_invalid": retry_on_error, "retries": retries, @@ -307,7 +316,8 @@ def tcp(ctx, host, port, framer): if framer == "rtu": kwargs["framer"] = ModbusRtuFramer client = ModbusTcpClient(**kwargs) - cli(client) + cli = CLI(client) + cli.run() @main.command("serial") @@ -427,7 +437,8 @@ def serial( # pylint: disable=too-many-arguments write_timeout=write_timeout, **ctx.obj, ) - cli(client) + cli = CLI(client) + cli.run() if __name__ == "__main__": diff --git a/pymodbus/repl/client/mclient.py b/pymodbus/repl/client/mclient.py index fae34aa06..69900c4e3 100644 --- a/pymodbus/repl/client/mclient.py +++ b/pymodbus/repl/client/mclient.py @@ -54,7 +54,7 @@ def handle_brodcast(func): def _wrapper(*args, **kwargs): self = args[0] resp = func(*args, **kwargs) - if not kwargs.get("unit") and self.params.broadcast_enable: + if not kwargs.get("slave") and self.params.broadcast_enable: return {"broadcasted": True} if not resp.isError(): return make_response_dict(resp) @@ -71,11 +71,7 @@ class ExtendedRequestSupport: # pylint: disable=(too-many-public-methods @staticmethod def _process_exception(resp, **kwargs): """Set internal process exception.""" - unit = kwargs.get("unit") - if ( - unit # pylint: disable=compare-to-zero,disable=consider-using-assignment-expr - == 0 - ): + if "slave" not in kwargs: err = {"message": "Broadcast message, ignoring errors!!!"} else: if isinstance(resp, ExceptionResponse): # pylint: disable=else-if-used @@ -99,7 +95,7 @@ def read_coils(self, address, count=1, slave=Defaults.Slave, **kwargs): :param address: The starting address to read from :param count: The number of coils to read - :param slave: Modbus slave unit ID + :param slave: Modbus slave slave ID :param kwargs: :returns: List of register values """ @@ -108,14 +104,14 @@ def read_coils(self, address, count=1, slave=Defaults.Slave, **kwargs): ) if not resp.isError(): return {"function_code": resp.function_code, "bits": resp.bits} - return ExtendedRequestSupport._process_exception(resp) + return ExtendedRequestSupport._process_exception(resp, slave=slave) def read_discrete_inputs(self, address, count=1, slave=Defaults.Slave, **kwargs): """Read `count` number of discrete inputs starting at offset `address`. :param address: The starting address to read from :param count: The number of coils to read - :param slave: Modbus slave unit ID + :param slave: Modbus slave slave ID :param kwargs: :return: List of bits """ @@ -124,7 +120,7 @@ def read_discrete_inputs(self, address, count=1, slave=Defaults.Slave, **kwargs) ) if not resp.isError(): return {"function_code": resp.function_code, "bits": resp.bits} - return ExtendedRequestSupport._process_exception(resp) + return ExtendedRequestSupport._process_exception(resp, slave=slave) @handle_brodcast def write_coil(self, address, value, slave=Defaults.Slave, **kwargs): @@ -132,7 +128,7 @@ def write_coil(self, address, value, slave=Defaults.Slave, **kwargs): :param address: coil offset to write to :param value: bit value to write - :param slave: Modbus slave unit ID + :param slave: Modbus slave slave ID :param kwargs: :return: """ @@ -147,7 +143,7 @@ def write_coils(self, address, values, slave=Defaults.Slave, **kwargs): :param address: coil offset to write to :param values: list of bit values to write (comma separated) - :param slave: Modbus slave unit ID + :param slave: Modbus slave slave ID :param kwargs: :return: """ @@ -162,7 +158,7 @@ def write_register(self, address, value, slave=Defaults.Slave, **kwargs): :param address: register offset to write to :param value: register value to write - :param slave: Modbus slave unit ID + :param slave: Modbus slave slave ID :param kwargs: :return: """ @@ -177,7 +173,7 @@ def write_registers(self, address, values, slave=Defaults.Slave, **kwargs): :param address: register offset to write to :param values: list of register value to write (comma separated) - :param slave: Modbus slave unit ID + :param slave: Modbus slave slave ID :param kwargs: :return: """ @@ -191,7 +187,7 @@ def read_holding_registers(self, address, count=1, slave=Defaults.Slave, **kwarg :param address: starting register offset to read from :param count: Number of registers to read - :param slave: Modbus slave unit ID + :param slave: Modbus slave slave ID :param kwargs: :return: """ @@ -200,14 +196,14 @@ def read_holding_registers(self, address, count=1, slave=Defaults.Slave, **kwarg ) if not resp.isError(): return {"function_code": resp.function_code, "registers": resp.registers} - return ExtendedRequestSupport._process_exception(resp) + return ExtendedRequestSupport._process_exception(resp, slave=slave) def read_input_registers(self, address, count=1, slave=Defaults.Slave, **kwargs): """Read `count` number of input registers starting at `address`. :param address: starting register offset to read from to :param count: Number of registers to read - :param slave: Modbus slave unit ID + :param slave: Modbus slave slave ID :param kwargs: :return: """ @@ -216,7 +212,7 @@ def read_input_registers(self, address, count=1, slave=Defaults.Slave, **kwargs) ) if not resp.isError(): return {"function_code": resp.function_code, "registers": resp.registers} - return ExtendedRequestSupport._process_exception(resp) + return ExtendedRequestSupport._process_exception(resp, slave=slave) def readwrite_registers( self, @@ -236,7 +232,7 @@ def readwrite_registers( :param read_count: Number of registers to read :param write_address: register offset to write to :param values: List of register values to write (comma separated) - :param slave: Modbus slave unit ID + :param slave: Modbus slave slave ID :param kwargs: :return: """ @@ -250,7 +246,7 @@ def readwrite_registers( ) if not resp.isError(): return {"function_code": resp.function_code, "registers": resp.registers} - return ExtendedRequestSupport._process_exception(resp) + return ExtendedRequestSupport._process_exception(resp, slave=slave) def mask_write_register( self, @@ -265,7 +261,7 @@ def mask_write_register( :param address: Reference address of register :param and_mask: And Mask :param or_mask: OR Mask - :param slave: Modbus slave unit ID + :param slave: Modbus slave slave ID :param kwargs: :return: """ @@ -279,7 +275,7 @@ def mask_write_register( "and mask": resp.and_mask, "or mask": resp.or_mask, } - return ExtendedRequestSupport._process_exception(resp) + return ExtendedRequestSupport._process_exception(resp, slave=slave) def read_device_information(self, read_code=None, object_id=0x00, **kwargs): """Read the identification and additional information of remote slave. @@ -301,12 +297,12 @@ def read_device_information(self, read_code=None, object_id=0x00, **kwargs): "more follows": resp.more_follows, "space left": resp.space_left, } - return ExtendedRequestSupport._process_exception(resp) + return ExtendedRequestSupport._process_exception(resp, slave=request.slave_id) def report_slave_id(self, slave=Defaults.Slave, **kwargs): """Report information about remote slave ID. - :param slave: Modbus slave unit ID + :param slave: Modbus slave ID :param kwargs: :return: """ @@ -319,12 +315,12 @@ def report_slave_id(self, slave=Defaults.Slave, **kwargs): "status": resp.status, "byte count": resp.byte_count, } - return ExtendedRequestSupport._process_exception(resp) + return ExtendedRequestSupport._process_exception(resp, slave=slave) def read_exception_status(self, slave=Defaults.Slave, **kwargs): """Read contents of eight Exception Status output in a remote device. - :param slave: Modbus slave unit ID + :param slave: Modbus slave ID :param kwargs: :return: """ @@ -332,7 +328,7 @@ def read_exception_status(self, slave=Defaults.Slave, **kwargs): resp = self.execute(request) # pylint: disable=no-member if not resp.isError(): return {"function_code": resp.function_code, "status": resp.status} - return ExtendedRequestSupport._process_exception(resp) + return ExtendedRequestSupport._process_exception(resp, slave=request.slave_id) def get_com_event_counter(self, **kwargs): """Read status word and an event count. @@ -350,7 +346,7 @@ def get_com_event_counter(self, **kwargs): "status": resp.status, "count": resp.count, } - return ExtendedRequestSupport._process_exception(resp) + return ExtendedRequestSupport._process_exception(resp, slave=request.slave_id) def get_com_event_log(self, **kwargs): """Read status word. @@ -371,7 +367,7 @@ def get_com_event_log(self, **kwargs): "event count": resp.event_count, "events": resp.events, } - return ExtendedRequestSupport._process_exception(resp) + return ExtendedRequestSupport._process_exception(resp, slave=request.slave_id) def _execute_diagnostic_request(self, request): """Execute diagnostic request.""" @@ -382,7 +378,7 @@ def _execute_diagnostic_request(self, request): "sub function code": resp.sub_function_code, "message": resp.message, } - return ExtendedRequestSupport._process_exception(resp) + return ExtendedRequestSupport._process_exception(resp, slave=request.slave_id) def return_query_data(self, message=0, **kwargs): """Loop back data sent in response. diff --git a/pymodbus/repl/server/README.md b/pymodbus/repl/server/README.md index 9c1ead4e9..5b408276e 100644 --- a/pymodbus/repl/server/README.md +++ b/pymodbus/repl/server/README.md @@ -64,7 +64,7 @@ docker run -it pymodbus-dev/pymodbus pymodbus.server --help │ --modbus-server -s TEXT Modbus Server [default: ModbusServerTypes.tcp] │ │ --framer -f TEXT Modbus framer to use [default: ModbusFramerTypes.socket] │ │ --modbus-port -p INTEGER Modbus port [default: 5020] │ -│ --unit-id -u INTEGER Supported Modbus unit id's [default: None] │ +│ --slave-id -u INTEGER Supported Modbus slave id's [default: None] │ │ --modbus-config-path PATH Path to additional modbus server config [default: None] │ │ --random -r INTEGER Randomize every `r` reads. 0=never, 1=always,2=every-second-read, and so on. Applicable │ │ IR and DI. │ @@ -81,7 +81,7 @@ Don't forget to restart the terminal for the auto-completion to kick-in. Use `TA Example usage. ```shell -✗ pymodbus.server run --modbus-server tcp --framer socket --unit-id 1 --unit-id 4 --random 2 +✗ pymodbus.server run --modbus-server tcp --framer socket --slave-id 1 --slave-id 4 --random 2 __________ .______. _________ \______ \___.__. _____ ____ __| _/\_ |__ __ __ ______ / _____/ ______________ __ ___________ @@ -121,7 +121,7 @@ Usage: manipulator response_type=|normal|error|delayed|empty|stray To run the Reactive server in the non-repl mode use `--no-repl` flag while starting the server. The server responses can still be manipulated with REST API calls. ``` -pymodbus.server --no-repl --verbose run --modbus-server tcp --framer socket --unit-id 1 --unit-id 4 --random 2 --modbus-port 5020 +pymodbus.server --no-repl --verbose run --modbus-server tcp --framer socket --slave-id 1 --slave-id 4 --random 2 --modbus-port 5020 2022-10-27 13:32:56,062 MainThread INFO main :246 Modbus server started Reactive Modbus Server started. diff --git a/pymodbus/repl/server/cli.py b/pymodbus/repl/server/cli.py index ac08233e5..5c449a7c9 100644 --- a/pymodbus/repl/server/cli.py +++ b/pymodbus/repl/server/cli.py @@ -105,8 +105,8 @@ def print_help(): ) -async def interactive_shell(server): # pylint: disable=too-complex - """Run CLI interactive shell.""" +def print_title(): + """Print title - large if there are sufficient columns, otherwise small.""" col = get_terminal_width() max_len = max( # pylint: disable=consider-using-generator [len(t) for t in TITLE.split("\n")] @@ -117,6 +117,11 @@ async def interactive_shell(server): # pylint: disable=too-complex print_formatted_text( HTML(f'') ) + + +async def interactive_shell(server): + """Run CLI interactive shell.""" + print_title() info("") completer = NestedCompleter.from_nested_dict(COMMANDS) session = PromptSession( @@ -124,9 +129,8 @@ async def interactive_shell(server): # pylint: disable=too-complex ) # Run echo loop. Read text from stdin, and reply it back. - while True: # pylint: disable=too-many-nested-blocks + while True: try: - invalid_command = False result = await session.prompt_async() if result == "exit": await server.web_app.shutdown() @@ -139,51 +143,13 @@ async def interactive_shell(server): # pylint: disable=too-complex continue if command := result.split(): if command[0] not in COMMANDS: - invalid_command = True - if invalid_command: warning(f"Invalid command or invalid usage of command - {command}") continue if len(command) == 1: warning(f'Usage: "{USAGE}"') else: - args = command[1:] - skip_next = False - val_dict = {} - for index, arg in enumerate(args): - if skip_next: - skip_next = False - continue - if "=" in arg: - arg, value = arg.split("=") - elif arg in COMMAND_ARGS: - try: - value = args[index + 1] - skip_next = True - except IndexError: - error(f"Missing value for argument - {arg}") - warning('Usage: "{USAGE}"') - break - valid = True - if arg == "response_type": - if value not in RESPONSE_TYPES: - warning(f"Invalid response type request - {value}") - warning(f"Choose from {RESPONSE_TYPES}") - valid = False - elif arg in { # pylint: disable=confusing-consecutive-elif - "error_code", - "delay_by", - "clear_after", - "data_len", - }: - try: - value = int(value) - except ValueError: - warning(f"Expected integer value for {arg}") - valid = False - - if valid: - val_dict[arg] = value - if val_dict: + val_dict = _process_args(command[1:]) + if val_dict: # pylint: disable=consider-using-assignment-expr server.update_manipulator_config(val_dict) # server.manipulator_config = val_dict # result = await run_command(tester, *command) @@ -192,6 +158,44 @@ async def interactive_shell(server): # pylint: disable=too-complex return +def _process_args(args) -> dict: + """Process arguments passed to CLI.""" + skip_next = False + val_dict = {} + for index, arg in enumerate(args): + if skip_next: + skip_next = False + continue + if "=" in arg: + arg, value = arg.split("=") + elif arg in COMMAND_ARGS: + try: + value = args[index + 1] + skip_next = True + except IndexError: + error(f"Missing value for argument - {arg}") + warning('Usage: "{USAGE}"') + break + if arg == "response_type": + if value not in RESPONSE_TYPES: + warning(f"Invalid response type request - {value}") + warning(f"Choose from {RESPONSE_TYPES}") + continue + elif arg in { # pylint: disable=confusing-consecutive-elif + "error_code", + "delay_by", + "clear_after", + "data_len", + }: + try: + value = int(value) + except ValueError: + warning(f"Expected integer value for {arg}") + continue + val_dict[arg] = value + return val_dict + + async def main(server): """Run main.""" # with patch_stdout(): diff --git a/pymodbus/repl/server/main.py b/pymodbus/repl/server/main.py index 2d983112b..1c72e9b0a 100644 --- a/pymodbus/repl/server/main.py +++ b/pymodbus/repl/server/main.py @@ -138,8 +138,8 @@ def run( help="Modbus framer to use", ), modbus_port: int = typer.Option(5020, "--modbus-port", "-p", help="Modbus port"), - modbus_unit_id: List[int] = typer.Option( - None, "--unit-id", "-u", help="Supported Modbus unit id's" + modbus_slave_id: List[int] = typer.Option( + [1], "--slave-id", "-u", help="Supported Modbus slave id's" ), modbus_config_path: Path = typer.Option( None, help="Path to additional modbus server config" @@ -191,22 +191,19 @@ def run( modbus_server, framer, modbus_port=modbus_port, - unit=modbus_unit_id, + slave=modbus_slave_id, loop=loop, single=False, data_block_settings=data_block_settings, **web_app_config, **modbus_config, ) - try: + if repl: + loop.run_until_complete(run_repl(app)) + else: loop.run_until_complete(app.run_async(repl)) - if repl: - loop.run_until_complete(run_repl(app)) loop.run_forever() - except CANCELLED_ERROR: - print("Done!!!!!") - if __name__ == "__main__": app() diff --git a/pymodbus/server/__init__.py b/pymodbus/server/__init__.py index e9f22d0a0..70793baa8 100644 --- a/pymodbus/server/__init__.py +++ b/pymodbus/server/__init__.py @@ -2,30 +2,7 @@ import external classes, to make them easier to use: """ -from pymodbus.server.async_io import ( - ModbusSerialServer, - ModbusTcpServer, - ModbusTlsServer, - ModbusUdpServer, - ModbusUnixServer, - ServerAsyncStop, - ServerStop, - StartAsyncSerialServer, - StartAsyncTcpServer, - StartAsyncTlsServer, - StartAsyncUdpServer, - StartAsyncUnixServer, - StartSerialServer, - StartTcpServer, - StartTlsServer, - StartUdpServer, -) -from pymodbus.server.simulator.http_server import ModbusSimulatorServer - -# ---------------------------------------------------------------------------# -# Exported symbols -# ---------------------------------------------------------------------------# __all__ = [ "ModbusSerialServer", "ModbusSimulatorServer", @@ -45,3 +22,23 @@ "StartTlsServer", "StartUdpServer", ] + +from pymodbus.server.async_io import ( + ModbusSerialServer, + ModbusTcpServer, + ModbusTlsServer, + ModbusUdpServer, + ModbusUnixServer, + ServerAsyncStop, + ServerStop, + StartAsyncSerialServer, + StartAsyncTcpServer, + StartAsyncTlsServer, + StartAsyncUdpServer, + StartAsyncUnixServer, + StartSerialServer, + StartTcpServer, + StartTlsServer, + StartUdpServer, +) +from pymodbus.server.simulator.http_server import ModbusSimulatorServer diff --git a/pymodbus/server/async_io.py b/pymodbus/server/async_io.py index 0e1d6c757..edaf9a106 100644 --- a/pymodbus/server/async_io.py +++ b/pymodbus/server/async_io.py @@ -4,9 +4,9 @@ import ssl import time import traceback +from contextlib import suppress from typing import Union -from pymodbus.client.serial_asyncio import create_serial_connection from pymodbus.constants import Defaults from pymodbus.datastore import ModbusServerContext from pymodbus.device import ModbusControlBlock, ModbusDeviceIdentification @@ -20,12 +20,11 @@ ModbusSocketFramer, ModbusTlsFramer, ) +from pymodbus.transport.serial_asyncio import create_serial_connection -try: +with suppress(ImportError): import serial -except ImportError: - pass def sslctx_provider( @@ -164,7 +163,7 @@ def connection_lost(self, call_exc): traceback.format_exc(), ) - async def handle(self): # pylint: disable=too-complex + async def handle(self): """Return Asyncio coroutine which represents a single conversation. between the modbus slave and master @@ -193,7 +192,7 @@ async def handle(self): # pylint: disable=too-complex reset_frame = False while self.running: try: - units = self.server.context.slaves() + slaves = self.server.context.slaves() # this is an asyncio.Queue await, it will never fail data = await self._recv_() if isinstance(data, tuple): @@ -202,13 +201,11 @@ async def handle(self): # pylint: disable=too-complex else: addr = (None,) # empty tuple - if not isinstance(units, (list, tuple)): - units = [units] # if broadcast is enabled make sure to # process requests to address 0 if self.server.broadcast_enable: # pragma: no cover - if 0 not in units: - units.append(0) + if 0 not in slaves: + slaves.append(0) Log.debug("Handling data: {}", data, ":hex") @@ -216,7 +213,7 @@ async def handle(self): # pylint: disable=too-complex self.framer.processIncomingPacket( data=data, callback=lambda x: self.execute(x, *addr), - unit=units, + slave=slaves, single=single, ) @@ -256,17 +253,17 @@ def execute(self, request, *addr): broadcast = False try: - if self.server.broadcast_enable and not request.unit_id: + if self.server.broadcast_enable and not request.slave_id: broadcast = True # if broadcasting then execute on all slave contexts, # note response will be ignored - for unit_id in self.server.context.slaves(): - response = request.execute(self.server.context[unit_id]) + for slave_id in self.server.context.slaves(): + response = request.execute(self.server.context[slave_id]) else: - context = self.server.context[request.unit_id] + context = self.server.context[request.slave_id] response = request.execute(context) except NoSuchSlaveException: - Log.error("requested slave does not exist: {}", request.unit_id) + Log.error("requested slave does not exist: {}", request.slave_id) if self.server.ignore_missing_slaves: return # the client will simply timeout waiting for a response response = request.doException(merror.GatewayNoResponse) @@ -280,7 +277,7 @@ def execute(self, request, *addr): # no response when broadcasting if not broadcast: response.transaction_id = request.transaction_id - response.unit_id = request.unit_id + response.slave_id = request.slave_id skip_encoding = False if self.server.response_manipulator: response, skip_encoding = self.server.response_manipulator(response) @@ -389,7 +386,7 @@ class ModbusDisconnectedRequestHandler( def __init__(self, owner): """Initialize.""" super().__init__(owner) - _future = asyncio.get_running_loop().create_future() + _future = asyncio.Future() self.server.on_connection_terminated = _future def connection_lost(self, call_exc): @@ -513,8 +510,8 @@ def __init__( reuse of an address. :param ignore_missing_slaves: True to not send errors on a request to a missing slave - :param broadcast_enable: True to treat unit_id 0 as broadcast address, - False to treat 0 as any other unit_id + :param broadcast_enable: True to treat slave_id 0 as broadcast address, + False to treat 0 as any other slave_id :param response_manipulator: Callback method for manipulating the response """ @@ -536,7 +533,7 @@ def __init__( self.control.Identity.update(identity) # asyncio future that will be done once server has started - self.serving = self.loop.create_future() + self.serving = asyncio.Future() # constructors cannot be declared async, so we have to # defer the initialization of the server self.server = None @@ -552,6 +549,7 @@ async def serve_forever(self): self.path, ) self.serving.set_result(True) + Log.info("Server(Unix) listening.") await self.server.serve_forever() except asyncio.exceptions.CancelledError: raise @@ -595,7 +593,6 @@ def __init__( address=None, handler=None, allow_reuse_address=False, - defer_start=False, backlog=20, **kwargs, ): @@ -618,8 +615,8 @@ def __init__( connections are being made and broken to your Modbus slave :param ignore_missing_slaves: True to not send errors on a request to a missing slave - :param broadcast_enable: True to treat unit_id 0 as broadcast address, - False to treat 0 as any other unit_id + :param broadcast_enable: True to treat slave_id 0 as broadcast address, + False to treat 0 as any other slave_id :param response_manipulator: Callback method for manipulating the response """ @@ -643,14 +640,14 @@ def __init__( self.control.Identity.update(identity) # asyncio future that will be done once server has started - self.serving = self.loop.create_future() + self.serving = asyncio.Future() # constructors cannot be declared async, so we have to # defer the initialization of the server self.server = None self.factory_parms = { "reuse_address": allow_reuse_address, "backlog": backlog, - "start_serving": not defer_start, + "start_serving": True, } async def serve_forever(self): @@ -662,6 +659,7 @@ async def serve_forever(self): **self.factory_parms, ) self.serving.set_result(True) + Log.info("Server(TCP) listening.") try: await self.server.serve_forever() except asyncio.exceptions.CancelledError: @@ -715,7 +713,6 @@ def __init__( # pylint: disable=too-many-arguments reqclicert=False, handler=None, allow_reuse_address=False, - defer_start=False, backlog=20, **kwargs, ): @@ -744,8 +741,8 @@ def __init__( # pylint: disable=too-many-arguments connections are being made and broken to your Modbus slave :param ignore_missing_slaves: True to not send errors on a request to a missing slave - :param broadcast_enable: True to treat unit_id 0 as broadcast address, - False to treat 0 as any other unit_id + :param broadcast_enable: True to treat slave_id 0 as broadcast address, + False to treat 0 as any other slave_id :param response_manipulator: Callback method for manipulating the response """ @@ -756,7 +753,6 @@ def __init__( # pylint: disable=too-many-arguments address=address, handler=handler, allow_reuse_address=allow_reuse_address, - defer_start=defer_start, backlog=backlog, **kwargs, ) @@ -779,7 +775,6 @@ def __init__( identity=None, address=None, handler=None, - defer_start=False, backlog=20, **kwargs, ): @@ -796,13 +791,12 @@ def __init__( ModbusDisonnectedRequestHandler :param ignore_missing_slaves: True to not send errors on a request to a missing slave - :param broadcast_enable: True to treat unit_id 0 as broadcast address, - False to treat 0 as any other unit_id + :param broadcast_enable: True to treat slave_id 0 as broadcast address, + False to treat 0 as any other slave_id :param response_manipulator: Callback method for manipulating the response """ # TO BE REMOVED: - self.defer_start = defer_start self.backlog = backlog # ---------------- self.loop = asyncio.get_running_loop() @@ -824,8 +818,9 @@ def __init__( self.protocol = None self.endpoint = None self.on_connection_terminated = None + self.stop_serving = self.loop.create_future() # asyncio future that will be done once server has started - self.serving = self.loop.create_future() + self.serving = asyncio.Future() self.factory_parms = { "local_addr": self.address, "allow_broadcast": True, @@ -845,7 +840,9 @@ async def serve_forever(self): except Exception as exc: Log.error("Server unexpected exception {}", exc) raise RuntimeError(exc) from exc + Log.info("Server(UDP) listening.") self.serving.set_result(True) + await self.stop_serving else: raise RuntimeError( "Can't call serve_forever on an already running server object" @@ -859,10 +856,13 @@ async def server_close(self): """Close server.""" if self.endpoint: self.endpoint.running = False + if not self.stop_serving.done(): + self.stop_serving.set_result(True) if self.endpoint is not None and self.endpoint.handler_task is not None: self.endpoint.handler_task.cancel() if self.protocol is not None: self.protocol.close() + # TBD await self.protocol.wait_closed() self.protocol = None @@ -895,8 +895,8 @@ def __init__( :param handle_local_echo: (optional) Discard local echo from dongle. :param ignore_missing_slaves: True to not send errors on a request to a missing slave - :param broadcast_enable: True to treat unit_id 0 as broadcast address, - False to treat 0 as any other unit_id + :param broadcast_enable: True to treat slave_id 0 as broadcast address, + False to treat 0 as any other slave_id :param auto_reconnect: True to enable automatic reconnection, False otherwise :param reconnect_delay: reconnect delay in seconds @@ -1019,6 +1019,7 @@ async def serve_forever(self): raise RuntimeError( "Can't call serve_forever on an already running server object" ) + Log.info("Server(Serial) listening.") if self.device.startswith("socket:"): # Socket server means listen so start a socket server parts = self.device[9:].split(":") @@ -1068,10 +1069,8 @@ async def run(cls, server, custom_functions): for func in custom_functions: server.decoder.register(func) cls.active_server = _serverList(server) - try: + with suppress(asyncio.exceptions.CancelledError): await server.serve_forever() - except asyncio.CancelledError: - pass @classmethod async def async_stop(cls): diff --git a/pymodbus/server/reactive/default_config.py b/pymodbus/server/reactive/default_config.py index 90718339c..0c861757a 100644 --- a/pymodbus/server/reactive/default_config.py +++ b/pymodbus/server/reactive/default_config.py @@ -64,5 +64,3 @@ }, }, } - -__all__ = ["DEFAULT_CONFIG"] diff --git a/pymodbus/server/reactive/main.py b/pymodbus/server/reactive/main.py index 322566f43..9c8b5942c 100644 --- a/pymodbus/server/reactive/main.py +++ b/pymodbus/server/reactive/main.py @@ -164,7 +164,7 @@ def getValues(self, fc_as_hex, address, count=1): :returns: The requested values from a:a+c """ if not self.zero_mode: - address = address + 1 + address += 1 Log.debug("getValues: fc-[{}] address-{}: count-{}", fc_as_hex, address, count) _block_type = self.decode(fc_as_hex) if self._randomize > 0 and _block_type in {"d", "i"}: @@ -215,7 +215,7 @@ def __init__(self, host, port, modbus_server): self._add_routes() self._counter = 0 self._modbus_server.response_manipulator = self.manipulate_response - self._manipulator_config = dict(**DEFAULT_MANIPULATOR) + self._manipulator_config = {**DEFAULT_MANIPULATOR} self._web_app.on_startup.append(self.start_modbus_server) self._web_app.on_shutdown.append(self.stop_modbus_server) @@ -322,7 +322,7 @@ def manipulate_response(self, response): Log.warning("Sending error response for all incoming requests") err_response = ExceptionResponse(response.function_code, error_code) err_response.transaction_id = response.transaction_id - err_response.unit_id = response.unit_id + err_response.slave_id = response.slave_id response = err_response self._counter += 1 elif response_type == "delayed": @@ -396,7 +396,7 @@ def create_identity( def create_context( cls, data_block_settings: dict = {}, - unit: list[int] | int = [1], + slave: list[int] | int = [1], single: bool = False, randomize: int = 0, change_rate: int = 0, @@ -404,17 +404,17 @@ def create_context( """Create Modbus context. :param data_block_settings: Datablock (dict) Refer DEFAULT_DATA_BLOCK - :param unit: Unit id for the slave + :param slave: Unit id for the slave :param single: To run as a single slave :param randomize: Randomize every reads for DI and IR. :param change_rate: Rate in % of registers to change for DI and IR. :return: ModbusServerContext object """ data_block = data_block_settings.pop("data_block", DEFAULT_DATA_BLOCK) - if not isinstance(unit, list): - unit = [unit] + if not isinstance(slave, list): + slave = [slave] slaves = {} - for i in unit: + for i in slave: block = {} for modbus_entity, block_desc in data_block.items(): start_address = block_desc.get("block_start", 0) @@ -454,7 +454,7 @@ def factory( # pylint: disable=dangerous-default-value,too-many-arguments server, framer=None, context=None, - unit=1, + slave=1, single=False, host="localhost", modbus_port=5020, @@ -468,7 +468,7 @@ def factory( # pylint: disable=dangerous-default-value,too-many-arguments :param server: Modbus server type (tcp, rtu, tls, udp) :param framer: Modbus framer (ModbusSocketFramer, ModbusRTUFramer, ModbusTLSFramer) :param context: Modbus server context to use - :param unit: Modbus unit id + :param slave: Modbus slave id :param single: Run in single mode :param host: Host address to use for both web app and modbus server (default localhost) :param modbus_port: Modbus port for TCP and UDP server(default: 5020) @@ -493,7 +493,7 @@ def factory( # pylint: disable=dangerous-default-value,too-many-arguments if not context: context = cls.create_context( data_block_settings=data_block_settings, - unit=unit, + slave=slave, single=single, randomize=randomize, change_rate=change_rate, @@ -509,7 +509,6 @@ def factory( # pylint: disable=dangerous-default-value,too-many-arguments framer=framer, identity=identity, address=(host, modbus_port), - defer_start=False, **kwargs, ) return ReactiveServer(host, web_port, server) diff --git a/pymodbus/server/simulator/http_server.py b/pymodbus/server/simulator/http_server.py index 68328960d..02e3c4ea9 100644 --- a/pymodbus/server/simulator/http_server.py +++ b/pymodbus/server/simulator/http_server.py @@ -598,7 +598,7 @@ def server_response_manipulator(self, response): self.call_list.append(tracer) self.call_monitor.trace_response = False - if not self.call_response.active == RESPONSE_INACTIVE: + if self.call_response.active != RESPONSE_INACTIVE: return response, False skip_encoding = False @@ -622,7 +622,7 @@ def server_response_manipulator(self, response): response.function_code, self.call_response.error_response ) err_response.transaction_id = response.transaction_id - err_response.unit_id = response.unit_id + err_response.slave_id = response.slave_id elif self.call_response.active == RESPONSE_JUNK: response = os.urandom(self.call_response.junk_len) skip_encoding = True diff --git a/pymodbus/server/simulator/main.py b/pymodbus/server/simulator/main.py index 09af4928a..ef75a839a 100755 --- a/pymodbus/server/simulator/main.py +++ b/pymodbus/server/simulator/main.py @@ -41,6 +41,7 @@ """ import argparse import asyncio +import os from pymodbus import pymodbus_apply_logging_config from pymodbus.logging import Log @@ -71,6 +72,7 @@ def get_commandline(): "--http_port", help="use as port to bind http listen", type=str, + default=8081, ) parser.add_argument( "--log", @@ -83,6 +85,7 @@ def get_commandline(): "--json_file", help='name of json file, default is "setup.json"', type=str, + default=os.path.join(os.path.dirname(__file__), "setup.json"), ) parser.add_argument( "--log_file", @@ -109,8 +112,6 @@ def get_commandline(): async def run_main(): """Run server async.""" cmd_args = get_commandline() - cmd_args["http_port"] = 8081 - cmd_args["json_file"] = "./pymodbus/server/simulator/setup.json" task = ModbusSimulatorServer(**cmd_args) await task.run_forever() diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index bacaccdf6..6a0fc3565 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -1,8 +1,19 @@ """Collection of transaction based abstractions.""" + +__all__ = [ + "FifoTransactionManager", + "DictTransactionManager", + "ModbusSocketFramer", + "ModbusTlsFramer", + "ModbusRtuFramer", + "ModbusAsciiFramer", + "ModbusBinaryFramer", +] + # pylint: disable=missing-type-doc -import socket import struct import time +from contextlib import suppress from functools import partial from threading import RLock @@ -104,7 +115,7 @@ def _validate_response(self, request, response, exp_resp_len): mbap = self.client.framer.decode_data(response) if ( - mbap.get("unit") != request.unit_id + mbap.get("slave") != request.slave_id or mbap.get("fcode") & 0x7F != request.function_code ): return False @@ -113,7 +124,7 @@ def _validate_response(self, request, response, exp_resp_len): return mbap.get("length") == exp_resp_len return True - def execute(self, request): # pylint: disable=too-complex + def execute(self, request): # noqa: C901 """Start the producer to send the next request to consumer.write(Frame(request)).""" with self._transaction_lock: try: @@ -130,7 +141,7 @@ def execute(self, request): # pylint: disable=too-complex Log.debug("Clearing current Frame: - {}", _buffer) self.client.framer.resetFrame() if broadcast := ( - self.client.params.broadcast_enable and not request.unit_id + self.client.params.broadcast_enable and not request.slave_id ): self._transact(request, None, broadcast=True) response = b"Broadcast write sent - no response expected" @@ -140,13 +151,13 @@ def execute(self, request): # pylint: disable=too-complex if hasattr(request, "get_response_pdu_size"): response_pdu_size = request.get_response_pdu_size() if isinstance(self.client.framer, ModbusAsciiFramer): - response_pdu_size = response_pdu_size * 2 + response_pdu_size *= 2 if response_pdu_size: expected_response_length = ( self._calculate_response_length(response_pdu_size) ) if ( # pylint: disable=simplifiable-if-statement - request.unit_id in self._no_response_devices + request.slave_id in self._no_response_devices ): full = True else: @@ -168,15 +179,15 @@ def execute(self, request): # pylint: disable=too-complex ) if valid_response: if ( - request.unit_id in self._no_response_devices + request.slave_id in self._no_response_devices and response ): - self._no_response_devices.remove(request.unit_id) + self._no_response_devices.remove(request.slave_id) Log.debug("Got response!!!") break if not response: - if request.unit_id not in self._no_response_devices: - self._no_response_devices.append(request.unit_id) + 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, @@ -206,14 +217,17 @@ def execute(self, request): # pylint: disable=too-complex tid=request.transaction_id, ) self.client.framer.processIncomingPacket( - response, addTransaction, request.unit_id + response, + addTransaction, + request.slave_id, + tid=request.transaction_id, ) if not (response := self.getTransaction(request.transaction_id)): if len(self.transactions): response = self.getTransaction(tid=0) else: last_exception = last_exception or ( - "No Response received from the remote unit" + "No Response received from the remote slave" "/Unable to decode response" ) response = ModbusIOException( @@ -306,11 +320,7 @@ def _transact(self, packet, response_length, full=False, broadcast=False): result = self._recv(response_length, full) # result2 = self._recv(response_length, full) Log.debug("RECV: {}", result, ":hex") - except ( - socket.error, - ModbusIOException, - InvalidMessageReceivedException, - ) as msg: + except (OSError, ModbusIOException, InvalidMessageReceivedException) as msg: if self.reset_socket: self.client.close() Log.debug("Transaction failed. ({}) ", msg) @@ -322,7 +332,7 @@ def _send(self, packet, _retrying=False): """Send.""" return self.client.framer.sendPacket(packet) - def _recv(self, expected_response_length, full): # pylint: disable=too-complex + def _recv(self, expected_response_length, full): # noqa: C901 """Receive.""" total = None if not full: @@ -368,15 +378,14 @@ def _recv(self, expected_response_length, full): # pylint: disable=too-complex elif expected_response_length is None and isinstance( self.client.framer, ModbusRtuFramer ): - try: + with suppress( + IndexError # response length indeterminate with available bytes + ): expected_response_length = ( self.client.framer.get_expected_response_length( read_min ) ) - except IndexError: - # Could not determine response length with available bytes - pass if expected_response_length is not None: expected_response_length -= min_size total = expected_response_length + min_size @@ -569,19 +578,3 @@ def delTransaction(self, tid): Log.debug("Deleting transaction {}", tid) if self.transactions: self.transactions.pop(0) - - -# --------------------------------------------------------------------------- # -# Exported symbols -# --------------------------------------------------------------------------- # - - -__all__ = [ - "FifoTransactionManager", - "DictTransactionManager", - "ModbusSocketFramer", - "ModbusTlsFramer", - "ModbusRtuFramer", - "ModbusAsciiFramer", - "ModbusBinaryFramer", -] diff --git a/pymodbus/transport/__init__.py b/pymodbus/transport/__init__.py new file mode 100644 index 000000000..2d5c29eaa --- /dev/null +++ b/pymodbus/transport/__init__.py @@ -0,0 +1,7 @@ +"""Transport.""" + +__all__ = [ + "BaseTransport", +] + +from pymodbus.transport.transport import BaseTransport diff --git a/pymodbus/client/serial_asyncio/README.md b/pymodbus/transport/serial_asyncio/README.md similarity index 99% rename from pymodbus/client/serial_asyncio/README.md rename to pymodbus/transport/serial_asyncio/README.md index a521b632c..6c9ee80ff 100644 --- a/pymodbus/client/serial_asyncio/README.md +++ b/pymodbus/transport/serial_asyncio/README.md @@ -1,4 +1,3 @@ Patch for serial_asyncio, also submitted to the repo: https://github.com/pyserial/pyserial-asyncio/pull/94 - diff --git a/pymodbus/client/serial_asyncio/__init__.py b/pymodbus/transport/serial_asyncio/__init__.py similarity index 95% rename from pymodbus/client/serial_asyncio/__init__.py rename to pymodbus/transport/serial_asyncio/__init__.py index 393c7b79b..a52ef7b28 100644 --- a/pymodbus/client/serial_asyncio/__init__.py +++ b/pymodbus/transport/serial_asyncio/__init__.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- # # Implementation of asyncio support. # @@ -16,12 +15,13 @@ implementation. It should be possible to get that working though. """ import asyncio +import contextlib import os -try: + +with contextlib.suppress(ImportError): import serial -except ImportError: - pass + try: import termios @@ -41,7 +41,7 @@ class SerialTransport(asyncio.Transport): indeed a serial port. - You generally won’t instantiate a transport yourself; instead, you + You generally won`t instantiate a transport yourself; instead, you will call `create_serial_connection` which will create the transport and try to initiate the underlying communication channel, calling you back when it succeeds. @@ -129,7 +129,8 @@ def write(self, data): This method does not block; it buffers the data and arranges for it to be sent out asynchronously. Writes made after the - transport has been closed will be ignored.""" + transport has been closed will be ignored. + """ if self._closing: return @@ -151,7 +152,7 @@ def can_write_eof(self): def pause_reading(self): """Pause the receiving end of the transport. - No data will be passed to the protocol’s data_received() method + No data will be passed to the protocol`s data_received() method until resume_reading() is called. """ self._remove_reader() @@ -167,7 +168,7 @@ def resume_reading(self): def set_write_buffer_limits(self, high=None, low=None): """Set the high- and low-water limits for write flow control. - These two values control when the protocol’s + These two values control when the protocol`s pause_writing()and resume_writing() methods are called. If specified, the low-water limit must be less than or equal to the high-water limit. Neither high nor low can be negative. @@ -197,7 +198,7 @@ def abort(self): self._abort(None) def flush(self): - """clears output buffer and stops any more data being written""" + """Clears output buffer and stops any more data being written""" self._remove_writer() self._write_buffer.clear() self._maybe_resume_protocol() @@ -361,7 +362,7 @@ def _set_write_buffer_limits(self, high=None, low=None): if low is None: low = high // 4 if not high >= low >= 0: - raise ValueError("high (%r) must be >= low (%r) must be >= 0" % (high, low)) + raise ValueError(f"high ({high!r}) must be >= low ({low!r}) must be >= 0") self._high_water = high self._low_water = low @@ -422,20 +423,19 @@ def _call_connection_lost(self, exc): assert self._closing assert not self._has_writer assert not self._has_reader - try: - self._serial.flush() - except (serial.SerialException if os.name == "nt" else termios.error): - # ignore serial errors which may happen if the serial device was - # hot-unplugged. - pass - try: - self._protocol.connection_lost(exc) - finally: - self._write_buffer.clear() + if self._serial: + with contextlib.suppress(Exception): + self._serial.flush() + self._serial.close() self._serial = None - self._protocol = None - self._loop = None + if self._protocol: + with contextlib.suppress(Exception): + self._protocol.connection_lost(exc) + + self._write_buffer.clear() + self._write_buffer.clear() + self._loop = None async def create_serial_connection(loop, protocol_factory, *args, **kwargs): diff --git a/pymodbus/transport/transport.py b/pymodbus/transport/transport.py new file mode 100644 index 000000000..b03222b9a --- /dev/null +++ b/pymodbus/transport/transport.py @@ -0,0 +1,402 @@ +"""Base for all transport types.""" +# mypy: disable-error-code="name-defined" +# needed because asyncio.Server is not defined (to mypy) in v3.8.16 +from __future__ import annotations + +import asyncio +import ssl +import sys +from contextlib import suppress +from dataclasses import dataclass +from typing import Any, Callable, Coroutine + +from pymodbus.framer import ModbusFramer +from pymodbus.logging import Log +from pymodbus.transport.serial_asyncio import create_serial_connection + + +class BaseTransport: + """Base class for transport types. + + BaseTransport contains functions common to all transport types and client/server. + + This class is not available in the pymodbus API, and should not be referenced in Applications. + """ + + @dataclass + class CommParamsClass: + """Parameter class.""" + + # generic + done: bool = False + comm_name: str = None + reconnect_delay: float = None + reconnect_delay_max: float = None + timeout_connect: float = None + framer: ModbusFramer = None + + # tcp / tls / udp / serial + host: str = None + + # tcp / tls / udp + port: int = None + + # tls + ssl: ssl.SSLContext = None + server_hostname: str = None + + # serial + baudrate: int = None + bytesize: int = None + parity: str = None + stopbits: int = None + + def check_done(self): + """Check if already setup""" + if self.done: + raise RuntimeError("Already setup!") + self.done = True + + def __init__( + self, + comm_name: str, + reconnect_delay: tuple[int, int], + timeout_connect: int, + framer: ModbusFramer, + callback_connected: Callable[[], None], + callback_disconnected: Callable[[Exception], None], + callback_data: Callable[[bytes], int], + ) -> None: + """Initialize a transport instance. + + :param comm_name: name of this transport connection + :param reconnect_delay: delay and max in milliseconds for first reconnect (0,0 for no reconnect) + :param timeout_connect: Max. time in milliseconds for connect to complete + :param framer: Modbus framer to decode/encode messagees. + :param callback_connected: Called when connection is established + :param callback_disconnected: Called when connection is disconnected + :param callback_data: Called when data is received + """ + self.cb_connection_made = callback_connected + self.cb_connection_lost = callback_disconnected + self.cb_handle_data = callback_data + + # properties, can be read, but may not be mingled with + self.comm_params = self.CommParamsClass( + comm_name=comm_name, + reconnect_delay=reconnect_delay[0] / 1000, + reconnect_delay_max=reconnect_delay[1] / 1000, + timeout_connect=timeout_connect / 1000, + framer=framer, + ) + + self.reconnect_delay_current: float = 0 + self.transport: asyncio.BaseTransport | asyncio.Server = None + self.protocol: asyncio.BaseProtocol = None + with suppress(RuntimeError): + self.loop: asyncio.AbstractEventLoop = asyncio.get_running_loop() + self.reconnect_timer: asyncio.TimerHandle = None + self.recv_buffer: bytes = b"" + self.call_connect_listen: Callable[[], Coroutine[Any, Any, Any]] = lambda: None + self.use_udp = False + + # ----------------------------- # + # Transport specific parameters # + # ----------------------------- # + def setup_unix(self, setup_server: bool, host: str): + """Prepare transport unix""" + if sys.platform.startswith("win"): + raise RuntimeError("Modbus_unix is not supported on Windows!") + self.comm_params.check_done() + self.comm_params.done = True + self.comm_params.host = host + if setup_server: + self.call_connect_listen = lambda: self.loop.create_unix_server( + self.handle_listen, + path=self.comm_params.host, + start_serving=True, + ) + else: + self.call_connect_listen = lambda: self.loop.create_unix_connection( + lambda: self, + path=self.comm_params.host, + ) + + def setup_tcp(self, setup_server: bool, host: str, port: int): + """Prepare transport tcp""" + self.comm_params.check_done() + self.comm_params.done = True + self.comm_params.host = host + self.comm_params.port = port + if setup_server: + self.call_connect_listen = lambda: self.loop.create_server( + self.handle_listen, + host=self.comm_params.host, + port=self.comm_params.port, + reuse_address=True, + start_serving=True, + ) + else: + self.call_connect_listen = lambda: self.loop.create_connection( + lambda: self, + host=self.comm_params.host, + port=self.comm_params.port, + ) + + def setup_tls( + self, + setup_server: bool, + host: str, + port: int, + sslctx: ssl.SSLContext, + certfile: str, + keyfile: str, + password: str, + server_hostname: str, + ): + """Prepare transport tls""" + self.comm_params.check_done() + self.comm_params.done = True + self.comm_params.host = host + self.comm_params.port = port + self.comm_params.server_hostname = server_hostname + if not sslctx: + # According to MODBUS/TCP Security Protocol Specification, it is + # TLSv2 at least + sslctx = ssl.SSLContext( + ssl.PROTOCOL_TLS_SERVER if setup_server else ssl.PROTOCOL_TLS_CLIENT + ) + sslctx.check_hostname = False + sslctx.verify_mode = ssl.CERT_NONE + sslctx.options |= ssl.OP_NO_TLSv1_1 + sslctx.options |= ssl.OP_NO_TLSv1 + sslctx.options |= ssl.OP_NO_SSLv3 + sslctx.options |= ssl.OP_NO_SSLv2 + if certfile: + sslctx.load_cert_chain( + certfile=certfile, keyfile=keyfile, password=password + ) + self.comm_params.ssl = sslctx + if setup_server: + self.call_connect_listen = lambda: self.loop.create_server( + self.handle_listen, + host=self.comm_params.host, + port=self.comm_params.port, + reuse_address=True, + ssl=self.comm_params.ssl, + start_serving=True, + ) + else: + self.call_connect_listen = lambda: self.loop.create_connection( + lambda: self, + self.comm_params.host, + self.comm_params.port, + ssl=self.comm_params.ssl, + server_hostname=self.comm_params.server_hostname, + ) + + def setup_udp(self, setup_server: bool, host: str, port: int): + """Prepare transport udp""" + self.comm_params.check_done() + self.comm_params.done = True + self.comm_params.host = host + self.comm_params.port = port + if setup_server: + + async def call_async_listen(self): + """Remove protocol return value.""" + transport, _protocol = await self.loop.create_datagram_endpoint( + self.handle_listen, + local_addr=(self.comm_params.host, self.comm_params.port), + ) + return transport + + self.call_connect_listen = lambda: call_async_listen(self) + else: + self.call_connect_listen = lambda: self.loop.create_datagram_endpoint( + lambda: self, + remote_addr=(self.comm_params.host, self.comm_params.port), + ) + self.use_udp = True + + def setup_serial( + self, + setup_server: bool, + host: str, + baudrate: int, + bytesize: int, + parity: str, + stopbits: int, + ): + """Prepare transport serial""" + self.comm_params.check_done() + self.comm_params.done = True + self.comm_params.host = host + self.comm_params.baudrate = baudrate + self.comm_params.bytesize = bytesize + self.comm_params.parity = parity + self.comm_params.stopbits = stopbits + if setup_server: + self.call_connect_listen = lambda: create_serial_connection( + self.loop, + self.handle_listen, + self.comm_params.host, + baudrate=self.comm_params.baudrate, + bytesize=self.comm_params.bytesize, + parity=self.comm_params.parity, + stopbits=self.comm_params.stopbits, + timeout=self.comm_params.timeout_connect, + ) + + else: + self.call_connect_listen = lambda: create_serial_connection( + self.loop, + lambda: self, + self.comm_params.host, + baudrate=self.comm_params.baudrate, + bytesize=self.comm_params.bytesize, + stopbits=self.comm_params.stopbits, + parity=self.comm_params.parity, + timeout=self.comm_params.timeout_connect, + ) + + async def transport_connect(self): + """Handle generic connect and call on to specific transport connect.""" + Log.debug("Connecting {}", self.comm_params.comm_name) + try: + self.transport, self.protocol = await asyncio.wait_for( + self.call_connect_listen(), + timeout=self.comm_params.timeout_connect, + ) + except ( + asyncio.TimeoutError, + OSError, + ) as exc: + Log.warning("Failed to connect {}", exc) + self.close(reconnect=True) + return self.transport, self.protocol + + async def transport_listen(self): + """Handle generic listen and call on to specific transport listen.""" + Log.debug("Awaiting connections {}", self.comm_params.comm_name) + try: + self.transport = await self.call_connect_listen() + except OSError as exc: + Log.warning("Failed to start server {}", exc) + self.close() + return self.transport + + # ---------------------------------- # + # Transport asyncio standard methods # + # ---------------------------------- # + def connection_made(self, transport: asyncio.BaseTransport): + """Call from asyncio, when a connection is made. + + :param transport: socket etc. representing the connection. + """ + Log.debug("Connected to {}", self.comm_params.comm_name) + self.transport = transport + self.reset_delay() + self.cb_connection_made() + + def connection_lost(self, reason: Exception): + """Call from asyncio, when the connection is lost or closed. + + :param reason: None or an exception object + """ + Log.debug("Connection lost {} due to {}", self.comm_params.comm_name, reason) + self.cb_connection_lost(reason) + self.close(reconnect=True) + + def eof_received(self): + """Call when eof received (other end closed connection). + + Handling is moved to connection_lost() + """ + + def data_received(self, data: bytes): + """Call when some data is received. + + :param data: non-empty bytes object with incoming data. + """ + Log.debug("recv: {}", data, ":hex") + self.recv_buffer += data + cut = self.cb_handle_data(self.recv_buffer) + self.recv_buffer = self.recv_buffer[cut:] + + def datagram_received(self, data, _addr): + """Receive datagram (UDP connections).""" + self.data_received(data) + + # -------------------------------- # + # Helper methods for child classes # + # -------------------------------- # + async def send(self, data: bytes) -> bool: + """Send request. + + :param data: non-empty bytes object with data to send. + """ + Log.debug("send: {}", data, ":hex") + if self.use_udp: + return self.transport.sendto(data) # type: ignore[union-attr] + return self.transport.write(data) # type: ignore[union-attr] + + def close(self, reconnect: bool = False) -> None: + """Close connection. + + :param reconnect: (default false), try to reconnect + """ + if self.transport: + if hasattr(self.transport, "abort"): + self.transport.abort() + self.transport.close() + self.transport = None + self.protocol = None + if self.reconnect_timer: + self.reconnect_timer.cancel() + self.reconnect_timer = None + self.recv_buffer = b"" + + if not reconnect or not self.reconnect_delay_current: + self.reconnect_delay_current = 0 + return + + Log.debug( + "Waiting {} {} ms reconnecting.", + self.comm_params.comm_name, + self.reconnect_delay_current * 1000, + ) + self.reconnect_timer = self.loop.call_later( + self.reconnect_delay_current, + asyncio.create_task, + self.transport_connect(), + ) + self.reconnect_delay_current = min( + 2 * self.reconnect_delay_current, self.comm_params.reconnect_delay_max + ) + + def reset_delay(self) -> None: + """Reset wait time before next reconnect to minimal period.""" + self.reconnect_delay_current = self.comm_params.reconnect_delay + + # ---------------- # + # Internal methods # + # ---------------- # + def handle_listen(self): + """Handle incoming connect.""" + return self + + # ----------------- # + # The magic methods # + # ----------------- # + async def __aenter__(self): + """Implement the client with async enter block.""" + return self + + async def __aexit__(self, _class, _value, _traceback) -> None: + """Implement the client with async exit block.""" + self.close() + + def __str__(self) -> str: + """Build a string representation of the connection.""" + return f"{self.__class__.__name__}({self.comm_params.comm_name})" diff --git a/pymodbus/utilities.py b/pymodbus/utilities.py index bcb429a7c..911d89442 100644 --- a/pymodbus/utilities.py +++ b/pymodbus/utilities.py @@ -3,8 +3,21 @@ A collection of utilities for packing data, unpacking data computing checksums, and decode checksums. """ + +__all__ = [ + "pack_bitstring", + "unpack_bitstring", + "default", + "computeCRC", + "checkCRC", + "computeLRC", + "checkLRC", + "rtuFrameSize", +] + # pylint: disable=missing-type-doc import struct +from typing import List class ModbusTransactionState: # pylint: disable=too-few-public-methods @@ -94,10 +107,10 @@ def dict_property(store, index): # --------------------------------------------------------------------------- # # Bit packing functions # --------------------------------------------------------------------------- # -def pack_bitstring(bits): - """Create a string out of an array of bits. +def pack_bitstring(bits: List[bool]) -> bytes: + """Create a bytestring out of a list of bits. - :param bits: A bit array + :param bits: A list of bits example:: @@ -121,37 +134,26 @@ def pack_bitstring(bits): return ret -def unpack_bitstring(string): - """Create bit array out of a string. +def unpack_bitstring(data: bytes) -> List[bool]: + """Create bit list out of a bytestring. - :param string: The modbus data packet to decode + :param data: The modbus data packet to decode example:: bytes = "bytes to decode" result = unpack_bitstring(bytes) """ - byte_count = len(string) + byte_count = len(data) bits = [] for byte in range(byte_count): - value = int(int(string[byte])) + value = int(int(data[byte])) for _ in range(8): bits.append((value & 1) == 1) value >>= 1 return bits -def make_byte_string(byte_string): - """Return byte string from a given string, python3 specific fix. - - :param byte_string: - :return: - """ - if isinstance(byte_string, str): - byte_string = byte_string.encode() - return byte_string - - # --------------------------------------------------------------------------- # # Error Detection Functions # --------------------------------------------------------------------------- # @@ -266,18 +268,3 @@ def hexlify_packets(packet): if not packet: return "" return " ".join([hex(int(x)) for x in packet]) - - -# --------------------------------------------------------------------------- # -# Exported symbols -# --------------------------------------------------------------------------- # -__all__ = [ - "pack_bitstring", - "unpack_bitstring", - "default", - "computeCRC", - "checkCRC", - "computeLRC", - "checkLRC", - "rtuFrameSize", -] diff --git a/pymodbus/version.py b/pymodbus/version.py deleted file mode 100644 index 58ade0dc5..000000000 --- a/pymodbus/version.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Handle the version information here. - -you should only have to change the version tuple. -""" - - -class Version: - """Manage version.""" - - def __init__(self, package, major, minor, micro, pre=None): - """Initialize. - - :param package: Name of the package that this is a version of. - :param major: The major version number. - :param minor: The minor version number. - :param micro: The micro version number. - :param pre: The pre release tag - """ - self.package = package - self.major = major - self.minor = minor - self.micro = micro - self.pre = pre - - def short(self) -> str: - """Return a string in canonical short version format: ...
."""
-        pre = ""
-        if self.pre:
-            pre = f".{self.pre}"
-        return f"{self.major}.{self.minor}.{self.micro}{pre}"
-
-    def __str__(self) -> str:
-        """Return a string representation of the object.
-
-        :returns: A string representation of this object
-        """
-        return f"[{self.package}, version {self.short()}]"
-
-
-version = Version("pymodbus", 3, 2, 2, "")
diff --git a/requirements.txt b/requirements.txt
index 00885d88f..94748cf04 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -12,7 +12,6 @@
 # Required packages.
 # -------------------------------------------------------------------
 # install:required
-setuptools<66.0.0
 
 # -------------------------------------------------------------------
 # optional packages.
@@ -33,10 +32,6 @@ click>=8.0.0
 # install:serial
 pyserial>=3.5
 
-# install:datastore
-redis>=2.10.6
-sqlalchemy>=1.1.15
-
 
 # -------------------------------------------------------------------
 # documentation, everything needed to generate documentation.
@@ -51,24 +46,19 @@ sphinx-rtd-theme==1.1.1
 # development, everything needed to develop/test/check.
 # -------------------------------------------------------------------
 # install:development
-bandit==1.7.4
 codespell==2.2.2
 coverage==7.1.0
-flake8==6.0.0
-flake8-docstrings==1.7.0
-flake8-noqa==1.3.0
-flake8-comprehensions==3.10.1
-mypy==1.0.1
+mypy==1.3.0
 pre-commit==3.1.1
 pyflakes==3.0.1
 pydocstyle==6.3.0
 pycodestyle==2.10.0
-pylint==2.15.10
+pylint==2.17.2
 pytest==7.2.1
 pytest-asyncio==0.20.3
 pytest-cov==4.0.0
 pytest-timeout==2.1.0
 pytest-xdist==3.1.0
-types-redis
+ruff==0.0.261
 types-Pygments
 types-pyserial
diff --git a/ruff.toml b/ruff.toml
new file mode 100644
index 000000000..75cd36afe
--- /dev/null
+++ b/ruff.toml
@@ -0,0 +1,52 @@
+target-version="py38"
+exclude = [
+    "pymodbus/transport/serial_asyncio",
+    "venv",
+    ".venv",
+    ".git",
+    "build",
+]
+ignore = [
+    "D202",  # No blank lines allowed after function docstring (to work with black)
+    "D400",  # docstrings ending in period
+    "E501",  # line too long
+    "E731",  # lambda expressions
+    "PT019",  # Bug: https://github.com/m-burst/flake8-pytest-style/issues/202
+    "S101",  # Use of `assert`
+    "S311",  # PRNG for cryptography
+    "S104",  # binding on all interfaces
+]
+line-length = 120
+select = [
+    "B007",   # Loop control variable {name} not used within loop body
+    "B014",   # Exception handler with duplicate exception
+    "C",      # complexity
+    "D",      # docstrings
+    "E",      # pycodestyle errors
+    "F",      # pyflakes
+    "I",      # isort
+    "PGH",    # pygrep-hooks
+    "PLC",    # pylint
+    "PT",     # flake8-pytest-style
+    "RUF",    # ruff builtins
+    "S",      # bandit
+    "SIM105", # flake8-simplify
+    "SIM117", #
+    "SIM118", #
+    "SIM201", #
+    "SIM212", #
+    "SIM300", #
+    "SIM401", #
+    "UP",     # pyupgrade
+    "W",      # pycodestyle warnings
+    # "TRY",    # tryceratops
+    "TRY004", # Prefer TypeError exception for invalid type
+]
+[pydocstyle]
+convention = "pep257"
+[isort]
+lines-after-imports = 2
+known-local-folder = [
+    "common",
+    "contrib",
+]
diff --git a/setup.cfg b/setup.cfg
index 06579c5ab..bd5a1e976 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -65,20 +65,13 @@ include = pymodbus*
 
 
 [pylint.master]
-# Specify a configuration file.
-#rcfile=
-
-# Python code to execute.
-#init-hook=
-
-# Files or directories to be skipped.
-#ignore=CVS
 
 # Add files or directories matching the regex patterns to the ignore-list.
 ignore-paths=
-    examples/v2.5.3,
-    pymodbus/client/serial_asyncio,
+    pymodbus/transport/serial_asyncio,
     doc
+
+# Files or directories matching the regular expression patterns are skipped.
 ignore-patterns=^\.#
 
 # Pickle collected data for later comparisons.
@@ -97,12 +90,12 @@ load-plugins=
     pylint.extensions.emptystring,
     pylint.extensions.eq_without_hash,
     pylint.extensions.for_any_all,
-    pylint.extensions.mccabe,
     pylint.extensions.overlapping_exceptions,
     pylint.extensions.private_import,
     pylint.extensions.set_membership,
     pylint.extensions.typing,
 # NOT WANTED:
+#     pylint.extensions.mccabe,  (replaced by ruff)
 #     pylint.extensions.broad_try_clause,
 #     pylint.extensions.consider_ternary_expression,
 #     pylint.extensions.empty_comment,
@@ -112,32 +105,10 @@ load-plugins=
 # Use multiple processes to speed up Pylint, 0 will auto-detect.
 jobs=0
 
-# pylint would attempt to guess common misconfiguration.
-suggestion-mode=yes
-
-# Allow loading of arbitrary C extensions.
-unsafe-load-any-extension=no
-
-# package or module names from where C extensions may be loaded.
-extension-pkg-allow-list=
-
 # Minimum supported python version
-py-version = 3.8.0
-
-# Amount of potential inferred values with a single object.
-limit-inference-results=100
-
-# Specify a score threshold to be exceeded before program exits with error.
-fail-under=10.0
-
-# Return non-zero exit code if any of these messages/categories are detected.
-fail-on=
-
-
+py-version = 3.8
 
 [pylint.messages_control]
-# Only show warnings with the listed confidence levels.
-# confidence=
 
 # Enable/Disable the message/report/category/checker with the given id(s).
 enable=all
@@ -149,42 +120,16 @@ disable=
     suppressed-message, # NOT wanted
 
 [pylint.reports]
+
 # Set the output format.
 output-format=text
 
-# Tells whether to display a full report or only the messages
-reports=no
-
-# Python expression which should return a note less than 10.
-evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))
-
-# Template used to display messages.
-#msg-template=
-
-# Activate the evaluation score.
-score=yes
-
 [pylint.logging]
-# Logging modules to check the string format.
-logging-modules=logging
 
 # The type of string formatting that logging methods do.
 logging-format-style=new
 
-[pylint.miscellaneous]
-# List of note tags/regular expression to take in consideration.
-notes=FIXME,XXX,TODO
-#notes-rgx=
-
 [pylint.similarities]
-# Minimum lines number of a similarity.
-min-similarity-lines=4
-
-# Ignore comments when computing similarities.
-ignore-comments=yes
-
-# Ignore docstrings when computing similarities.
-ignore-docstrings=yes
 
 # Ignore imports when computing similarities.
 ignore-imports=no
@@ -192,180 +137,29 @@ ignore-imports=no
 # Signatures are removed from the similarity computation
 ignore-signatures=no
 
-
 [pylint.variables]
-# Tells whether we should check for unused import in __init__ files.
-init-import=no
 
 # A regular expression matching the name of dummy variables
-#dummy-variables-rgx=_$|dummy
 dummy-variables-rgx=
 
-# List of additional names supposed to be defined in builtins.
-additional-builtins=
-
-# List of strings which can identify a callback function by name.
-#callbacks=cb_,_cb
-callbacks=
-
-# Tells whether unused global variables should be treated as a violation.
-allow-global-unused-variables=yes
-
-# List of names allowed to shadow builtins
-allowed-redefined-builtins=
-
-# Argument names that match this expression will be ignored.
-ignored-argument-names=_.*
-
-# List of qualified module names which can have objects that can redefine
-# builtins.
-#redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
-redefining-builtins-modules=past.builtins,future.builtins,builtins,io
-
 [pylint.format]
-# Maximum number of characters on a single line.
-max-line-length=100
-
-# Regexp for a line that is allowed to be longer than the limit.
-ignore-long-lines=^\s*(# )??$
-
-# Allow the body of an if to be on the same line.
-single-line-if-stmt=no
-
-# Allow the body of a class to be on the same line as the declaration
-single-line-class-stmt=no
 
 # Maximum number of lines in a module
 max-module-lines=2000
 
-# String used as indentation unit.
-indent-string='    '
-indent-after-paren=4
-
-# Expected format of line ending.
-#expected-line-ending-format=
-
 [pylint.basic]
+
 # Good variable names which should always be accepted.
-#good-names=i,j,k,run,_
 good-names=i,j,k,rr,fc,rq,fd,x,_
-#good-names-rgxs=
 
-# Bad variable names which should always be refused, separated by a comma
-bad-names=foo,bar,baz,toto,tutu,tata
-bad-names-rgxs=
-
-# Colon-delimited sets of names that determine each other's naming style when
-# the name regexes allow several styles.
-name-group=
-
-# Include a hint for the correct naming format with invalid-name
-include-naming-hint=no
-
-# Naming style matching correct function names.
-function-naming-style=snake_case
-function-rgx=[a-z_][a-z0-9_]{2,30}$
-
-# Naming style matching correct variable names.
-variable-naming-style=snake_case
-variable-rgx=[a-z_][a-z0-9_]{2,30}$
-
-# Naming style matching correct constant names.
-const-naming-style=UPPER_CASE
-const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
-
-# Naming style matching correct attribute names.
-attr-naming-style=snake_case
+# Regular expression matching correct attribute names.
 attr-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
 
-# Naming style matching correct argument names.
-argument-naming-style=snake_case
-argument-rgx=[a-z_][a-z0-9_]{2,30}$
-
-# Naming style matching correct class attribute names.
-class-attribute-naming-style=any
-class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
-
-# Naming style matching correct class constant names.
-class-const-naming-style=UPPER_CASE
-#class-const-rgx=
-
-# Naming style matching correct inline iteration names.
-inlinevar-naming-style=any
-inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
-
-# Naming style matching correct class names.
-class-naming-style=PascalCase
-class-rgx=[A-Z_][a-zA-Z0-9]+$
-
-# Naming style matching correct module names.
-module-naming-style=snake_case
-module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
-
-# Naming style matching correct method names.
-method-naming-style=snake_case
+# Regular expression matching correct method names.
 method-rgx=[a-z_][a-zA-Z0-9_]{2,}$
 
-# function or class names that do not require a docstring.
-no-docstring-rgx=__.*__
-
-# Minimum line length for functions/classes that require docstrings.
-docstring-min-length=-1
-
-# List of decorators that define properties, such as abc.abstractproperty.
-property-classes=abc.abstractproperty
-
-[pylint.typecheck]
-# Regex pattern to define which classes are considered mixins
-mixin-class-rgx=.*MixIn
-
-# List of module names for which member attributes should not be checked.
-ignored-modules=
-
-# List of class names for which member attributes should not be checked.
-ignored-classes=SQLObject, optparse.Values, thread._local, _thread._local
-
-# List of members which are set dynamically.
-generated-members=REQUEST,acl_users,aq_parent,argparse.Namespace
-
-# List of decorators that create context managers from functions.
-contextmanager-decorators=contextlib.contextmanager
-
-# Warn about missing members when the attribute can be None.
-ignore-none=yes
-
-# This flag controls whether pylint should warn about no-member
-ignore-on-opaque-inference=yes
-
-# Show a hint with possible names when a member name was not found.
-missing-member-hint=yes
-
-# The minimum edit distance a name should have.
-missing-member-hint-distance=1
-
-# The total number of similar names that should be taken in consideration.
-missing-member-max-choices=1
-
-[pylint.spelling]
-# Spelling dictionary name.
-spelling-dict=
-
-# List of comma separated words that should not be checked.
-spelling-ignore-words=
-
-# List of comma separated words that should be considered directives.
-spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,isort:skip,mypy:
-
-# A path to a file that contains private dictionary; one word per line.
-spelling-private-dict-file=
-
-# Tells whether to store unknown words.
-spelling-store-unknown-words=no
-
-# Limits count of emitted suggestions for spelling mistakes.
-max-spelling-suggestions=4
-
 [pylint.design]
+
 # Maximum number of arguments for function / method
 max-args=10
 
@@ -381,125 +175,32 @@ max-branches=27
 # Maximum number of statements in function / method body
 max-statements=100
 
-# Maximum number of parents for a class (see R0901).
-max-parents=7
-
-# List of qualified class names to ignore when counting class parents.
-ignored-parents=
-
 # Maximum number of attributes for a class (see R0902).
 max-attributes=20
 
-# Minimum number of public methods for a class (see R0903).
-min-public-methods=2
-
 # Maximum number of public methods for a class (see R0904).
 max-public-methods=25
 
-# Maximum number of boolean expressions in an if statement (see R0916).
-max-bool-expr=5
-
-# Regular expressions of class ancestor names to ignore.
-exclude-too-few-public-methods=
-
 [pylint.classes]
-# List of method names used to declare (i.e. assign) instance attributes.
-defining-attr-methods=__init__,__new__,setUp,__post_init__
-
-# List of valid names for the first argument in a class method.
-valid-classmethod-first-arg=cls
 
 # List of valid names for the first argument in a metaclass class method.
 valid-metaclass-classmethod-first-arg=mcs
 
-# List of member names, which should be excluded from the protected access
-# warning.
-exclude-protected=_asdict,_fields,_replace,_source,_make
-
-# Warn about protected attribute access inside special methods
-check-protected-access-in-special-methods=no
-
 [pylint.imports]
-# List of modules that can be imported at any level.
-#allow-any-import-level=
-allow-any-import-level=no
-
-# Allow wildcard imports from modules that define __all__.
-allow-wildcard-with-all=no
-
-# Analyse import fallback blocks.
-analyse-fallback-blocks=no
 
 # Deprecated modules which should not be used, separated by a comma
 deprecated-modules=regsub,TERMIOS,Bastion,rexec
 
-# Create a graph of every dependencies.
-import-graph=
-ext-import-graph=
-int-import-graph=
-
-# import to recognize a module as a standard compatibility libraries.
-known-standard-library=
-
-# Force import order to recognize a module as part of a third party library.
-known-third-party=enchant
-
-# Couples of modules and preferred modules, separated by a comma.
-preferred-modules=
-
 [pylint.exceptions]
-# Exceptions that will emit a warning when being caught.
-overgeneral-exceptions=Exception
 
-[pylint.typing]
-# app / library does need runtime introspection of type annotations.
-runtime-typing = true
+# Exceptions that will emit a warning when being caught.
+overgeneral-exceptions=builtins.Exception
 
 [pylint.deprecated_builtins]
+
 # List of builtins function names that should not be used.
 bad-functions=map,input
 
-[pylint.refactoring]
-# Maximum number of nested blocks for function / method body
-max-nested-blocks=5
-
-# Complete name of functions that never returns.
-never-returning-functions=sys.exit,argparse.parse_error
-
-[pylint.string]
-# inconsistent-quotes generates a warning.
-check-quote-consistency=no
-
-# implicit-str-concat should generate a warning.
-check-str-concat-over-line-jumps=no
-
-[pylint.code_style]
-# Max line length for which to sill emit suggestions.
-#max-line-length-suggestions=
-
-[flake8]
-exclude         = pymodbus/client/serial_asyncio, venv,.venv,.git,build,examples/v2.5.3
-doctests        = True
-max-line-length = 120
-# To work with Black
-# D202 No blank lines allowed after function docstring
-# E203: Whitespace before ':'
-# E501: line too long
-# W503: Line break occurred before a binary operator
-# W504 line break after binary operator
-ignore =
-    D202,
-    E203,
-    E501,
-    W503,
-    W504,
-
-    D211,
-    D400,
-    E731,
-    W503
-noqa-require-code = True
-
 
 [mypy]
 strict_optional = False
@@ -550,6 +251,7 @@ upload_dir = build/sphinx/html
 testpaths = test
 addopts = -p no:warnings --dist loadgroup --numprocesses auto
 asyncio_mode = auto
+timeout = 40
 
 
 [coverage:run]
@@ -564,13 +266,3 @@ omit =
 [codespell]
 skip=./doc/_build,./doc/source/_static,venv,.venv,.git,htmlcov,CHANGELOG.rst,.mypy_cache
 ignore-words-list = asend
-
-[isort]
-skip=doc/_build,venv,.venv,.git,pymodbus/client/serial_asyncio
-py_version=38
-profile=black
-line_length = 79
-lines_after_imports = 2
-known_local_folder =
-    common
-    contrib
diff --git a/setup.py b/setup.py
index 7cd2ed605..54fb40ea0 100644
--- a/setup.py
+++ b/setup.py
@@ -8,11 +8,11 @@
 from setuptools import setup
 
 
-dependencies = {}
+dependencies: dict = {}
 with open("requirements.txt") as reqs:
     option = None
     for line in reqs.read().split("\n"):
-        if line == "":
+        if not line:
             option = None
         elif line.startswith("# install:"):
             option = line.split(":")[1]
@@ -30,5 +30,7 @@
 setup(
     install_requires=install_req,
     extras_require=dependencies,
-    package_data={"pymodbus": ["py.typed"]},
+    package_data={
+        "pymodbus": ["py.typed", "server/simulator/setup.json", "server/simulator/web/**/*"],
+    },
 )
diff --git a/test/conftest.py b/test/conftest.py
index f1514a852..2796e4e2b 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -1,6 +1,7 @@
 """Configure pytest."""
 import functools
 import platform
+from collections import deque
 
 import pytest
 
@@ -80,27 +81,17 @@ class mockSocket:  # pylint: disable=invalid-name
 
     timeout = 2
 
-    def __init__(self):
+    def __init__(self, copy_send=True):
         """Initialize."""
-        self.data = None
+        self.packets = deque()
+        self.buffer = None
         self.in_waiting = 0
+        self.copy_send = copy_send
 
-    def mock_store(self, msg):
+    def mock_prepare_receive(self, msg):
         """Store message."""
-        self.data = msg
-        self.in_waiting = len(self.data)
-
-    def mock_retrieve(self, size):
-        """Get message."""
-        if not self.data or not size:
-            return b""
-        if size >= len(self.data):
-            retval = self.data
-        else:
-            retval = self.data[0:size]
-        self.data = None
-        self.in_waiting = 0
-        return retval
+        self.packets.append(msg)
+        self.in_waiting += len(msg)
 
     def close(self):
         """Close."""
@@ -108,25 +99,38 @@ def close(self):
 
     def recv(self, size):
         """Receive."""
-        return self.mock_retrieve(size)
+        if not self.packets or not size:
+            return b""
+        if not self.buffer:
+            self.buffer = self.packets.popleft()
+        if size >= len(self.buffer):
+            retval = self.buffer
+            self.buffer = None
+        else:
+            retval = self.buffer[0:size]
+            self.buffer = self.buffer[size]
+        self.in_waiting -= len(retval)
+        return retval
 
     def read(self, size):
         """Read."""
-        return self.mock_retrieve(size)
+        return self.recv(size)
+
+    def recvfrom(self, size):
+        """Receive from."""
+        return [self.recv(size)]
 
     def send(self, msg):
         """Send."""
-        self.mock_store(msg)
+        if not self.copy_send:
+            return len(msg)
+        self.packets.append(msg)
+        self.in_waiting += len(msg)
         return len(msg)
 
-    def recvfrom(self, size):
-        """Receive from."""
-        return [self.mock_retrieve(size)]
-
     def sendto(self, msg, *_args):
         """Send to."""
-        self.mock_store(msg)
-        return len(msg)
+        return self.send(msg)
 
     def setblocking(self, _flag):
         """Set blocking."""
diff --git a/test/test_all_messages.py b/test/test_all_messages.py
index 172ce9da0..0f832e724 100644
--- a/test/test_all_messages.py
+++ b/test/test_all_messages.py
@@ -1,6 +1,4 @@
 """Test all messages."""
-import unittest
-
 from pymodbus.bit_read_message import (
     ReadCoilsRequest,
     ReadCoilsResponse,
@@ -35,69 +33,64 @@
 # ---------------------------------------------------------------------------#
 
 
-class ModbusAllMessagesTests(unittest.TestCase):
+class TestAllMessages:
     """All messages tests."""
 
     # -----------------------------------------------------------------------#
     #  Setup/TearDown
     # -----------------------------------------------------------------------#
 
-    def setUp(self):
-        """Initialize the test environment and builds request/result encoding pairs."""
-        arguments = {
-            "read_address": 1,
-            "read_count": 1,
-            "write_address": 1,
-            "write_registers": 1,
-        }
-        self.requests = [
-            lambda unit: ReadCoilsRequest(1, 5, unit=unit),
-            lambda unit: ReadDiscreteInputsRequest(1, 5, unit=unit),
-            lambda unit: WriteSingleCoilRequest(1, 1, unit=unit),
-            lambda unit: WriteMultipleCoilsRequest(1, [1], unit=unit),
-            lambda unit: ReadHoldingRegistersRequest(1, 5, unit=unit),
-            lambda unit: ReadInputRegistersRequest(1, 5, unit=unit),
-            lambda unit: ReadWriteMultipleRegistersRequest(unit=unit, **arguments),
-            lambda unit: WriteSingleRegisterRequest(1, 1, unit=unit),
-            lambda unit: WriteMultipleRegistersRequest(1, [1], unit=unit),
-        ]
-        self.responses = [
-            lambda unit: ReadCoilsResponse([1], unit=unit),
-            lambda unit: ReadDiscreteInputsResponse([1], unit=unit),
-            lambda unit: WriteSingleCoilResponse(1, 1, unit=unit),
-            lambda unit: WriteMultipleCoilsResponse(1, [1], unit=unit),
-            lambda unit: ReadHoldingRegistersResponse([1], unit=unit),
-            lambda unit: ReadInputRegistersResponse([1], unit=unit),
-            lambda unit: ReadWriteMultipleRegistersResponse([1], unit=unit),
-            lambda unit: WriteSingleRegisterResponse(1, 1, unit=unit),
-            lambda unit: WriteMultipleRegistersResponse(1, 1, unit=unit),
-        ]
-
-    def tearDown(self):
-        """Clean up the test environment"""
+    requests = [
+        lambda slave: ReadCoilsRequest(1, 5, slave=slave),
+        lambda slave: ReadDiscreteInputsRequest(1, 5, slave=slave),
+        lambda slave: WriteSingleCoilRequest(1, 1, slave=slave),
+        lambda slave: WriteMultipleCoilsRequest(1, [1], slave=slave),
+        lambda slave: ReadHoldingRegistersRequest(1, 5, slave=slave),
+        lambda slave: ReadInputRegistersRequest(1, 5, slave=slave),
+        lambda slave: ReadWriteMultipleRegistersRequest(
+            slave=slave,
+            read_address=1,
+            read_count=1,
+            write_address=1,
+            write_registers=1,
+        ),
+        lambda slave: WriteSingleRegisterRequest(1, 1, slave=slave),
+        lambda slave: WriteMultipleRegistersRequest(1, [1], slave=slave),
+    ]
+    responses = [
+        lambda slave: ReadCoilsResponse([1], slave=slave),
+        lambda slave: ReadDiscreteInputsResponse([1], slave=slave),
+        lambda slave: WriteSingleCoilResponse(1, 1, slave=slave),
+        lambda slave: WriteMultipleCoilsResponse(1, [1], slave=slave),
+        lambda slave: ReadHoldingRegistersResponse([1], slave=slave),
+        lambda slave: ReadInputRegistersResponse([1], slave=slave),
+        lambda slave: ReadWriteMultipleRegistersResponse([1], slave=slave),
+        lambda slave: WriteSingleRegisterResponse(1, 1, slave=slave),
+        lambda slave: WriteMultipleRegistersResponse(1, 1, slave=slave),
+    ]
 
     def test_initializing_slave_address_request(self):
-        """Test that every request can initialize the unit id"""
-        unit_id = 0x12
+        """Test that every request can initialize the slave id"""
+        slave_id = 0x12
         for factory in self.requests:
-            request = factory(unit_id)
-            self.assertEqual(request.unit_id, unit_id)
+            request = factory(slave_id)
+            assert request.slave_id == slave_id
 
     def test_initializing_slave_address_response(self):
-        """Test that every response can initialize the unit id"""
-        unit_id = 0x12
+        """Test that every response can initialize the slave id"""
+        slave_id = 0x12
         for factory in self.responses:
-            response = factory(unit_id)
-            self.assertEqual(response.unit_id, unit_id)
+            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, unit=0x12, transaction=0x12, protocol=0x12)
-        self.assertEqual(request.unit_id, 0x12)
-        self.assertEqual(request.transaction_id, 0x12)
-        self.assertEqual(request.protocol_id, 0x12)
+        request = ReadCoilsRequest(1, 5, slave=0x12, transaction=0x12, protocol=0x12)
+        assert request.slave_id == 0x12
+        assert request.transaction_id == 0x12
+        assert request.protocol_id == 0x12
 
         request = ReadCoilsRequest(1, 5)
-        self.assertEqual(request.unit_id, Defaults.Slave)
-        self.assertEqual(request.transaction_id, Defaults.TransactionId)
-        self.assertEqual(request.protocol_id, Defaults.ProtocolId)
+        assert request.slave_id == Defaults.Slave
+        assert request.transaction_id == Defaults.TransactionId
+        assert request.protocol_id == Defaults.ProtocolId
diff --git a/test/test_bit_read_messages.py b/test/test_bit_read_messages.py
index 5af92a65c..1e00723e3 100644
--- a/test/test_bit_read_messages.py
+++ b/test/test_bit_read_messages.py
@@ -7,7 +7,6 @@
 * Read Coils
 """
 import struct
-import unittest
 from test.conftest import MockContext
 
 from pymodbus.bit_read_message import (
@@ -26,7 +25,7 @@
 # ---------------------------------------------------------------------------#
 
 
-class ModbusBitMessageTests(unittest.TestCase):
+class TestModbusBitMessage:
     """Modbus bit read message tests."""
 
     # -----------------------------------------------------------------------#
@@ -43,19 +42,19 @@ def test_read_bit_base_class_methods(self):
         """Test basic bit message encoding/decoding"""
         handle = ReadBitsRequestBase(1, 1)
         msg = "ReadBitRequest(1,1)"
-        self.assertEqual(msg, str(handle))
+        assert msg == str(handle)
         handle = ReadBitsResponseBase([1, 1])
         msg = "ReadBitsResponseBase(2)"
-        self.assertEqual(msg, str(handle))
+        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)
             result = struct.pack(">HH", i, i)
-            self.assertEqual(handle.encode(), result)
+            assert handle.encode() == result
             handle.decode(result)
-            self.assertEqual((handle.address, handle.count), (i, i))
+            assert (handle.address, handle.count) == (i, i)
 
     def test_bit_read_base_response_encoding(self):
         """Test basic bit message encoding/decoding"""
@@ -64,7 +63,7 @@ def test_bit_read_base_response_encoding(self):
             handle = ReadBitsResponseBase(data)
             result = handle.encode()
             handle.decode(result)
-            self.assertEqual(handle.bits[:i], data)
+            assert handle.bits[:i] == data
 
     def test_bit_read_base_response_helper_methods(self):
         """Test the extra methods on a ReadBitsResponseBase"""
@@ -75,7 +74,7 @@ def test_bit_read_base_response_helper_methods(self):
         for i in (1, 3, 5):
             handle.resetBit(i)
         for i in range(8):
-            self.assertEqual(handle.getBit(i), False)
+            assert not handle.getBit(i)
 
     def test_bit_read_base_requests(self):
         """Test bit read request encoding"""
@@ -84,7 +83,7 @@ def test_bit_read_base_requests(self):
             ReadBitsResponseBase([1, 0, 1, 1, 0]): b"\x01\x0d",
         }
         for request, expected in iter(messages.items()):
-            self.assertEqual(request.encode(), expected)
+            assert request.encode() == expected
 
     def test_bit_read_message_execute_value_errors(self):
         """Test bit read request encoding"""
@@ -95,7 +94,7 @@ def test_bit_read_message_execute_value_errors(self):
         ]
         for request in requests:
             result = request.execute(context)
-            self.assertEqual(ModbusExceptions.IllegalValue, result.exception_code)
+            assert ModbusExceptions.IllegalValue == result.exception_code
 
     def test_bit_read_message_execute_address_errors(self):
         """Test bit read request encoding"""
@@ -106,7 +105,7 @@ def test_bit_read_message_execute_address_errors(self):
         ]
         for request in requests:
             result = request.execute(context)
-            self.assertEqual(ModbusExceptions.IllegalAddress, result.exception_code)
+            assert ModbusExceptions.IllegalAddress == result.exception_code
 
     def test_bit_read_message_execute_success(self):
         """Test bit read request encoding"""
@@ -118,7 +117,7 @@ def test_bit_read_message_execute_success(self):
         ]
         for request in requests:
             result = request.execute(context)
-            self.assertEqual(result.bits, [True] * 5)
+            assert result.bits == [True] * 5
 
     def test_bit_read_message_get_response_pdu(self):
         """Test bit read message get response pdu."""
@@ -132,4 +131,4 @@ def test_bit_read_message_get_response_pdu(self):
         }
         for request, expected in iter(requests.items()):
             pdu_len = request.get_response_pdu_size()
-            self.assertEqual(pdu_len, expected)
+            assert pdu_len == expected
diff --git a/test/test_bit_write_messages.py b/test/test_bit_write_messages.py
index d272f4f0d..6a42c42aa 100644
--- a/test/test_bit_write_messages.py
+++ b/test/test_bit_write_messages.py
@@ -6,7 +6,6 @@
 * Read/Write Discretes
 * Read Coils
 """
-import unittest
 from test.conftest import FakeList, MockContext
 
 from pymodbus.bit_write_message import (
@@ -23,7 +22,7 @@
 # ---------------------------------------------------------------------------#
 
 
-class ModbusBitMessageTests(unittest.TestCase):
+class TestModbusBitMessage:
     """Modbus bit write message tests."""
 
     # -----------------------------------------------------------------------#
@@ -47,56 +46,56 @@ def test_bit_write_base_requests(self):
             WriteMultipleCoilsResponse(1, 1): b"\x00\x01\x00\x01",
         }
         for request, expected in iter(messages.items()):
-            self.assertEqual(request.encode(), expected)
+            assert request.encode() == expected
 
     def test_bit_write_message_get_response_pdu(self):
         """Test bit write message."""
         requests = {WriteSingleCoilRequest(1, 0xABCD): 5}
         for request, expected in iter(requests.items()):
             pdu_len = request.get_response_pdu_size()
-            self.assertEqual(pdu_len, expected)
+            assert pdu_len == expected
 
     def test_write_multiple_coils_request(self):
         """Test write multiple coils."""
         request = WriteMultipleCoilsRequest(1, [True] * 5)
         request.decode(b"\x00\x01\x00\x05\x01\x1f")
-        self.assertEqual(request.byte_count, 1)
-        self.assertEqual(request.address, 1)
-        self.assertEqual(request.values, [True] * 5)
-        self.assertEqual(request.get_response_pdu_size(), 5)
+        assert request.byte_count == 1
+        assert request.address == 1
+        assert request.values == [True] * 5
+        assert request.get_response_pdu_size() == 5
 
         request = WriteMultipleCoilsRequest(1, True)
         request.decode(b"\x00\x01\x00\x01\x01\x01")
-        self.assertEqual(request.byte_count, 1)
-        self.assertEqual(request.address, 1)
-        self.assertEqual(request.values, [True])
-        self.assertEqual(request.get_response_pdu_size(), 5)
+        assert request.byte_count == 1
+        assert request.address == 1
+        assert request.values == [True]
+        assert request.get_response_pdu_size() == 5
 
     def test_invalid_write_multiple_coils_request(self):
         """Test write invalid multiple coils."""
         request = WriteMultipleCoilsRequest(1, None)
-        self.assertEqual(request.values, [])
+        assert request.values == []
 
     def test_write_single_coil_request_encode(self):
         """Test write single coil."""
         request = WriteSingleCoilRequest(1, False)
-        self.assertEqual(request.encode(), b"\x00\x01\x00\x00")
+        assert request.encode() == b"\x00\x01\x00\x00"
 
     def test_write_single_coil_execute(self):
         """Test write single coil."""
         context = MockContext(False, default=True)
         request = WriteSingleCoilRequest(2, True)
         result = request.execute(context)
-        self.assertEqual(result.exception_code, ModbusExceptions.IllegalAddress)
+        assert result.exception_code == ModbusExceptions.IllegalAddress
 
         context.valid = True
         result = request.execute(context)
-        self.assertEqual(result.encode(), b"\x00\x02\xff\x00")
+        assert result.encode() == b"\x00\x02\xff\x00"
 
         context = MockContext(True, default=False)
         request = WriteSingleCoilRequest(2, False)
         result = request.execute(context)
-        self.assertEqual(result.encode(), b"\x00\x02\x00\x00")
+        assert result.encode() == b"\x00\x02\x00\x00"
 
     def test_write_multiple_coils_execute(self):
         """Test write multiple coils."""
@@ -104,31 +103,31 @@ def test_write_multiple_coils_execute(self):
         # too many values
         request = WriteMultipleCoilsRequest(2, FakeList(0x123456))
         result = request.execute(context)
-        self.assertEqual(result.exception_code, ModbusExceptions.IllegalValue)
+        assert result.exception_code == ModbusExceptions.IllegalValue
 
         # bad byte count
         request = WriteMultipleCoilsRequest(2, [0x00] * 4)
         request.byte_count = 0x00
         result = request.execute(context)
-        self.assertEqual(result.exception_code, ModbusExceptions.IllegalValue)
+        assert result.exception_code == ModbusExceptions.IllegalValue
 
         # does not validate
         context.valid = False
         request = WriteMultipleCoilsRequest(2, [0x00] * 4)
         result = request.execute(context)
-        self.assertEqual(result.exception_code, ModbusExceptions.IllegalAddress)
+        assert result.exception_code == ModbusExceptions.IllegalAddress
 
         # validated request
         context.valid = True
         result = request.execute(context)
-        self.assertEqual(result.encode(), b"\x00\x02\x00\x04")
+        assert result.encode() == b"\x00\x02\x00\x04"
 
     def test_write_multiple_coils_response(self):
         """Test write multiple coils."""
         response = WriteMultipleCoilsResponse()
         response.decode(b"\x00\x80\x00\x08")
-        self.assertEqual(response.address, 0x80)
-        self.assertEqual(response.count, 0x08)
+        assert response.address == 0x80
+        assert response.count == 0x08
 
     def test_serializing_to_string(self):
         """Test serializing to string."""
@@ -140,4 +139,4 @@ def test_serializing_to_string(self):
         ]
         for request in requests:
             result = str(request)
-            self.assertTrue(result is not None and len(result))
+            assert result
diff --git a/test/test_client.py b/test/test_client.py
index 757975f96..8d36219fd 100755
--- a/test/test_client.py
+++ b/test/test_client.py
@@ -16,7 +16,7 @@
 from pymodbus.client.base import ModbusBaseClient
 from pymodbus.client.mixin import ModbusClientMixin
 from pymodbus.constants import Defaults
-from pymodbus.exceptions import ConnectionException, NotImplementedException
+from pymodbus.exceptions import ConnectionException
 from pymodbus.framer.ascii_framer import ModbusAsciiFramer
 from pymodbus.framer.rtu_framer import ModbusRtuFramer
 from pymodbus.framer.socket_framer import ModbusSocketFramer
@@ -38,7 +38,7 @@
     ],
 )
 @pytest.mark.parametrize(
-    "method, arg, pdu_request",
+    ("method", "arg", "pdu_request"),
     [
         ("read_coils", 1, pdu_bit_read.ReadCoilsRequest),
         ("read_discrete_inputs", 1, pdu_bit_read.ReadDiscreteInputsRequest),
@@ -206,7 +206,7 @@ def fake_execute(_self, request):
     ],
 )
 @pytest.mark.parametrize(
-    "type_args, clientclass",
+    ("type_args", "clientclass"),
     [
         # TBD ("serial", lib_client.AsyncModbusSerialClient),
         # TBD ("serial", lib_client.ModbusSerialClient),
@@ -239,9 +239,6 @@ async def test_client_instanciate(
         to_test = dict(arg_list["fix"]["opt_args"], **cur_args["opt_args"])
         to_test["host"] = cur_args["defaults"]["host"]
 
-    for arg, arg_test in to_test.items():
-        assert getattr(client.params, arg) == arg_test
-
     # Test information methods
     client.last_frame_end = 2
     client.silent_interval = 2
@@ -249,32 +246,24 @@ async def test_client_instanciate(
     client.last_frame_end = None
     assert not client.idle_time()
 
-    initial_delay = client.delay_ms
-    assert initial_delay > 0
-    client.delay_ms *= 2
-
-    assert client.delay_ms > initial_delay
-    client.reset_delay()
-    assert client.delay_ms == initial_delay
-
     rc1 = client._get_address_family("127.0.0.1")  # pylint: disable=protected-access
-    assert socket.AF_INET == rc1
+    assert rc1 == socket.AF_INET
     rc2 = client._get_address_family("::1")  # pylint: disable=protected-access
-    assert socket.AF_INET6 == rc2
+    assert rc2 == socket.AF_INET6
 
     # a successful execute
     client.connect = lambda: True
-    client._connected = True  # pylint: disable=protected-access
+    client.transport = lambda: None
     client.transaction = mock.Mock(**{"execute.return_value": True})
 
     # a unsuccessful connect
     client.connect = lambda: False
-    client._connected = False  # pylint: disable=protected-access
+    client.transport = None
     with pytest.raises(ConnectionException):
         client.execute()
 
 
-def test_client_modbusbaseclient():
+async def test_client_modbusbaseclient():
     """Test modbus base client class."""
     client = ModbusBaseClient(framer=ModbusAsciiFramer)
     client.register(pdu_bit_read.ReadCoilsResponse)
@@ -282,19 +271,11 @@ def test_client_modbusbaseclient():
     assert client.send(buffer) == buffer
     assert client.recv(10) == 10
 
-    with pytest.raises(NotImplementedException):
-        client.connect()
-    with pytest.raises(NotImplementedException):
-        client.is_socket_open()
-    with pytest.raises(NotImplementedException):
-        client.close()
-
     with mock.patch(
         "pymodbus.client.base.ModbusBaseClient.connect"
     ) as p_connect, mock.patch(
         "pymodbus.client.base.ModbusBaseClient.close"
     ) as p_close:
-
         p_connect.return_value = True
         p_close.return_value = True
         with ModbusBaseClient(framer=ModbusAsciiFramer) as b_client:
@@ -302,42 +283,30 @@ def test_client_modbusbaseclient():
         p_connect.return_value = False
 
 
-async def test_client_made_connection():
+async def test_client_connection_made():
     """Test protocol made connection."""
     client = lib_client.AsyncModbusTcpClient("127.0.0.1")
     assert not client.connected
-    client.client_made_connection(mock.sentinel.PROTOCOL)
+    client.connection_made(mock.sentinel.PROTOCOL)
     assert client.connected
 
-    client.client_made_connection(mock.sentinel.PROTOCOL_UNEXPECTED)
+    client.connection_made(mock.sentinel.PROTOCOL_UNEXPECTED)
     assert client.connected
 
 
-async def test_client_lost_connection():
+async def test_client_connection_lost():
     """Test protocol lost connection."""
     client = lib_client.AsyncModbusTcpClient("127.0.0.1")
     assert not client.connected
 
     # fake client is connected and *then* looses connection:
-    client.connected = True
     client.params.host = mock.sentinel.HOST
     client.params.port = mock.sentinel.PORT
-    with mock.patch(
-        "pymodbus.client.tcp.AsyncModbusTcpClient._launch_reconnect"
-    ) as mock_reconnect:
-        mock_reconnect.return_value = mock.sentinel.RECONNECT_GENERATOR
-
-        client.client_lost_connection(mock.sentinel.PROTOCOL_UNEXPECTED)
+    client.connection_lost(mock.sentinel.PROTOCOL_UNEXPECTED)
     assert not client.connected
-
-    client.connected = True
-    with mock.patch(
-        "pymodbus.client.tcp.AsyncModbusTcpClient._launch_reconnect"
-    ) as mock_reconnect:
-        mock_reconnect.return_value = mock.sentinel.RECONNECT_GENERATOR
-
-        client.client_lost_connection(mock.sentinel.PROTOCOL)
+    client.connection_lost(mock.sentinel.PROTOCOL)
     assert not client.connected
+    client.close()
 
 
 async def test_client_base_async():
@@ -347,60 +316,26 @@ async def test_client_base_async():
     ) as p_connect, mock.patch(
         "pymodbus.client.base.ModbusBaseClient.close"
     ) as p_close:
-
-        loop = asyncio.get_event_loop()
-        p_connect.return_value = loop.create_future()
+        asyncio.get_event_loop()
+        p_connect.return_value = asyncio.Future()
         p_connect.return_value.set_result(True)
-        p_close.return_value = loop.create_future()
+        p_close.return_value = asyncio.Future()
         p_close.return_value.set_result(True)
         async with ModbusBaseClient(framer=ModbusAsciiFramer) as client:
             str(client)
-        p_connect.return_value = loop.create_future()
+        p_connect.return_value = asyncio.Future()
         p_connect.return_value.set_result(False)
-        p_close.return_value = loop.create_future()
+        p_close.return_value = asyncio.Future()
         p_close.return_value.set_result(False)
 
 
-@pytest.mark.skip
-async def test_client_protocol():
-    """Test base modbus async client."""
-    base = ModbusBaseClient(framer=ModbusSocketFramer)
-    assert base.transport is None
-    assert not base.async_connected
-
-    base.connection_made(mock.sentinel.TRANSPORT)
-    assert base.transport is mock.sentinel.TRANSPORT
-    base.client_made_connection.assert_called_once_with(  # pylint: disable=no-member
-        base
-    )
-    assert not base.client_lost_connection.call_count  # pylint: disable=no-member
-
-    base.connection_lost(mock.sentinel.REASON)
-    assert base.transport is None
-    assert not base.client_made_connection.call_count  # pylint: disable=no-member
-    base.client_lost_connection.assert_called_once_with(  # pylint: disable=no-member
-        base
-    )
-    base.raise_future = mock.MagicMock()
-    request = mock.MagicMock()
-    base.transaction.addTransaction(request, 1)
-    base.connection_lost(mock.sentinel.REASON)
-    base.raise_future.assert_called_once()
-    call_args = base.raise_future.call_args.args
-    assert call_args[0] == request
-    assert isinstance(call_args[1], ConnectionException)
-    base.transport = mock.MagicMock()
-    base.transport = None
-    await base.async_close()
-
-
 async def test_client_protocol_receiver():
     """Test the client protocol data received"""
     base = ModbusBaseClient(framer=ModbusSocketFramer)
     transport = mock.MagicMock()
     base.connection_made(transport)
     assert base.transport == transport
-    assert base.async_connected
+    assert base.transport
     data = b"\x00\x00\x12\x34\x00\x06\xff\x01\x01\x02\x00\x04"
 
     # setup existing request
@@ -410,7 +345,7 @@ async def test_client_protocol_receiver():
     result = response.result()
     assert isinstance(result, pdu_bit_read.ReadCoilsResponse)
 
-    base._connected = False  # pylint: disable=protected-access
+    base.transport = None
     with pytest.raises(ConnectionException):
         await base._build_response(0x00)  # pylint: disable=protected-access
 
@@ -423,7 +358,7 @@ async def test_client_protocol_response():
     assert isinstance(excp, ConnectionException)
     assert not list(base.transaction)
 
-    base._connected = True  # pylint: disable=protected-access
+    base.transport = lambda: None
     base._build_response(0x00)  # pylint: disable=protected-access
     assert len(list(base.transaction)) == 1
 
@@ -443,13 +378,10 @@ async def test_client_protocol_handler():
     assert result == reply
 
 
+@pytest.mark.skip()
 async def test_client_protocol_execute():
     """Test the client protocol execute method"""
     base = ModbusBaseClient(host="127.0.0.1", framer=ModbusSocketFramer)
-    base.create_future = mock.MagicMock()
-    fut = asyncio.Future()
-    fut.set_result(fut)
-    base.create_future.return_value = fut
     transport = mock.MagicMock()
     base.connection_made(transport)
     base.transport.write = mock.Mock()
@@ -478,18 +410,23 @@ def test_client_udp_connect():
     """Test the Udp client connection method"""
     with mock.patch.object(socket, "socket") as mock_method:
 
-        class DummySocket:  # pylint: disable=too-few-public-methods
+        class DummySocket:
             """Dummy socket."""
 
+            fileno = 1
+
             def settimeout(self, *a, **kwa):
                 """Set timeout."""
 
+            def setblocking(self, _flag):
+                """Set blocking"""
+
         mock_method.return_value = DummySocket()
         client = lib_client.ModbusUdpClient("127.0.0.1")
         assert client.connect()
 
     with mock.patch.object(socket, "socket") as mock_method:
-        mock_method.side_effect = socket.error()
+        mock_method.side_effect = OSError()
         client = lib_client.ModbusUdpClient("127.0.0.1")
         assert not client.connect()
 
@@ -504,11 +441,29 @@ def test_client_tcp_connect():
         assert client.connect()
 
     with mock.patch.object(socket, "create_connection") as mock_method:
-        mock_method.side_effect = socket.error()
+        mock_method.side_effect = OSError()
         client = lib_client.ModbusTcpClient("127.0.0.1")
         assert not client.connect()
 
 
+def test_client_tcp_reuse():
+    """Test the tcp client connection method"""
+    with mock.patch.object(socket, "create_connection") as mock_method:
+        _socket = mock.MagicMock()
+        mock_method.return_value = _socket
+        client = lib_client.ModbusTcpClient("127.0.0.1")
+        _socket.getsockname.return_value = ("dmmy", 1234)
+        assert client.connect()
+    client.close()
+    with mock.patch.object(socket, "create_connection") as mock_method:
+        _socket = mock.MagicMock()
+        mock_method.return_value = _socket
+        client = lib_client.ModbusTcpClient("127.0.0.1")
+        _socket.getsockname.return_value = ("dmmy", 1234)
+        assert client.connect()
+    client.close()
+
+
 def test_client_tls_connect():
     """Test the tls client connection method"""
     with mock.patch.object(ssl.SSLSocket, "connect") as mock_method:
@@ -516,13 +471,13 @@ def test_client_tls_connect():
         assert client.connect()
 
     with mock.patch.object(socket, "create_connection") as mock_method:
-        mock_method.side_effect = socket.error()
+        mock_method.side_effect = OSError()
         client = lib_client.ModbusTlsClient("127.0.0.1")
         assert not client.connect()
 
 
 @pytest.mark.parametrize(
-    "datatype,value,registers",
+    ("datatype", "value", "registers"),
     [
         (ModbusClientMixin.DATATYPE.STRING, "abcd", [0x6162, 0x6364]),
         (ModbusClientMixin.DATATYPE.STRING, "a", [0x6100]),
diff --git a/test/test_client_faulty_response.py b/test/test_client_faulty_response.py
new file mode 100644
index 000000000..de15d4633
--- /dev/null
+++ b/test/test_client_faulty_response.py
@@ -0,0 +1,40 @@
+"""Test server working as slave on a multidrop RS485 line."""
+from unittest import mock
+
+import pytest
+
+from pymodbus.exceptions import ModbusIOException
+from pymodbus.factory import ClientDecoder
+from pymodbus.framer import ModbusSocketFramer
+
+
+class TestFaultyResponses:
+    """Test that server works on a multidrop line."""
+
+    slaves = [0]
+
+    good_frame = b"\x00\x01\x00\x00\x00\x05\x00\x03\x02\x00\x01"
+
+    @pytest.fixture(name="framer")
+    def fixture_framer(self):
+        """Prepare framer."""
+        return ModbusSocketFramer(ClientDecoder())
+
+    @pytest.fixture(name="callback")
+    def fixture_callback(self):
+        """Prepare dummy callback."""
+        return mock.Mock()
+
+    def test_ok_frame(self, framer, callback):
+        """Test ok frame."""
+        framer.processIncomingPacket(self.good_frame, callback, self.slaves)
+        callback.assert_called_once()
+
+    def test_faulty_frame1(self, framer, callback):
+        """Test ok frame."""
+        faulty_frame = b"\x00\x04\x00\x00\x00\x05\x00\x03\x0a\x00\x04"
+        with pytest.raises(ModbusIOException):
+            framer.processIncomingPacket(faulty_frame, callback, self.slaves)
+        callback.assert_not_called()
+        framer.processIncomingPacket(self.good_frame, callback, self.slaves)
+        callback.assert_called_once()
diff --git a/test/test_client_sync.py b/test/test_client_sync.py
index 3c6845212..d093e1915 100755
--- a/test/test_client_sync.py
+++ b/test/test_client_sync.py
@@ -1,10 +1,10 @@
 """Test client sync."""
 import ssl
-import unittest
 from itertools import count
 from test.conftest import mockSocket
-from unittest.mock import MagicMock, Mock, patch
+from unittest import mock
 
+import pytest
 import serial
 
 from pymodbus.client import (
@@ -29,9 +29,7 @@
 # ---------------------------------------------------------------------------#
 
 
-class SynchronousClientTest(
-    unittest.TestCase
-):  # pylint: disable=too-many-public-methods
+class TestSynchronousClient:  # pylint: disable=too-many-public-methods
     """Unittest for the pymodbus.client module."""
 
     # -----------------------------------------------------------------------#
@@ -43,49 +41,56 @@ def test_basic_syn_udp_client(self):
         # receive/send
         client = ModbusUdpClient("127.0.0.1")
         client.socket = mockSocket()
-        self.assertEqual(0, client.send(None))
-        self.assertEqual(1, client.send(b"\x50"))
-        self.assertEqual(b"\x50", client.recv(1))
+        assert not client.send(None)
+        assert client.send(b"\x50") == 1
+        assert client.recv(1) == b"\x50"
 
         # connect/disconnect
-        self.assertTrue(client.connect())
+        assert client.connect()
         client.close()
 
         # already closed socket
         client.socket = False
         client.close()
 
-        self.assertEqual("ModbusUdpClient(127.0.0.1:502)", str(client))
+        assert str(client) == "ModbusUdpClient(127.0.0.1:502)"
 
     def test_udp_client_is_socket_open(self):
         """Test the udp client is_socket_open method"""
         client = ModbusUdpClient("127.0.0.1")
-        self.assertTrue(client.is_socket_open())
+        assert client.is_socket_open()
 
     def test_udp_client_send(self):
         """Test the udp client send method"""
         client = ModbusUdpClient("127.0.0.1")
-        self.assertRaises(
-            ConnectionException,
-            lambda: client.send(None),
-        )
-
+        with pytest.raises(ConnectionException):
+            client.send(None)
         client.socket = mockSocket()
-        self.assertEqual(0, client.send(None))
-        self.assertEqual(4, client.send("1234"))
+        assert not client.send(None)
+        assert client.send("1234") == 4
 
     def test_udp_client_recv(self):
         """Test the udp client receive method"""
         client = ModbusUdpClient("127.0.0.1")
-        self.assertRaises(
-            ConnectionException,
-            lambda: client.recv(1024),
-        )
-
+        with pytest.raises(ConnectionException):
+            client.recv(1024)
         client.socket = mockSocket()
-        client.socket.mock_store(b"\x00" * 4)
-        self.assertEqual(b"", client.recv(0))
-        self.assertEqual(b"\x00" * 4, client.recv(4))
+        client.socket.mock_prepare_receive(b"\x00" * 4)
+        assert client.recv(0) == b""
+        assert client.recv(4) == b"\x00" * 4
+
+    def test_udp_client_recv_duplicate(self):
+        """Test the udp client receive method"""
+        test_msg = b"\x00\x01\x00\x00\x00\x05\x01\x04\x02\x00\x03"
+        client = ModbusUdpClient("127.0.0.1")
+        client.socket = mockSocket(copy_send=False)
+        client.socket.mock_prepare_receive(test_msg)
+        client.socket.mock_prepare_receive(test_msg)
+        reply_ok = client.read_input_registers(0x820, 1, 1)
+        assert not reply_ok.isError()
+        reply_none = client.read_input_registers(0x40, 10, 1)
+        assert reply_none.isError()
+        client.close()
 
     def test_udp_client_repr(self):
         """Test udp client representation."""
@@ -94,7 +99,7 @@ def test_udp_client_repr(self):
             f"<{client.__class__.__name__} at {hex(id(client))} socket={client.socket}, "
             f"ipaddr={client.params.host}, port={client.params.port}, timeout={client.params.timeout}>"
         )
-        self.assertEqual(repr(client), rep)
+        assert repr(client) == rep
 
     # -----------------------------------------------------------------------#
     # Test TCP Client
@@ -103,87 +108,79 @@ def test_udp_client_repr(self):
     def test_syn_tcp_client_instantiation(self):
         """Test sync tcp client."""
         client = ModbusTcpClient("127.0.0.1")
-        self.assertNotEqual(client, None)
+        assert client
 
-    @patch("pymodbus.client.tcp.select")
+    @mock.patch("pymodbus.client.tcp.select")
     def test_basic_syn_tcp_client(self, mock_select):
         """Test the basic methods for the tcp sync client"""
         # receive/send
         mock_select.select.return_value = [True]
         client = ModbusTcpClient("127.0.0.1")
         client.socket = mockSocket()
-        self.assertEqual(0, client.send(None))
-        self.assertEqual(1, client.send(b"\x45"))
-        self.assertEqual(b"\x45", client.recv(1))
+        assert not client.send(None)
+        assert client.send(b"\x45") == 1
+        assert client.recv(1) == b"\x45"
 
         # connect/disconnect
-        self.assertTrue(client.connect())
+        assert client.connect()
         client.close()
 
         # already closed socket
         client.socket = False
         client.close()
 
-        self.assertEqual("ModbusTcpClient(127.0.0.1:502)", str(client))
+        assert str(client) == "ModbusTcpClient(127.0.0.1:502)"
 
     def test_tcp_client_is_socket_open(self):
         """Test the tcp client is_socket_open method"""
         client = ModbusTcpClient("127.0.0.1")
-        self.assertFalse(client.is_socket_open())
+        assert not client.is_socket_open()
 
     def test_tcp_client_send(self):
         """Test the tcp client send method"""
         client = ModbusTcpClient("127.0.0.1")
-        self.assertRaises(
-            ConnectionException,
-            lambda: client.send(None),
-        )
-
+        with pytest.raises(ConnectionException):
+            client.send(None)
         client.socket = mockSocket()
-        self.assertEqual(0, client.send(None))
-        self.assertEqual(4, client.send("1234"))
+        assert not client.send(None)
+        assert client.send("1234") == 4
 
-    @patch("pymodbus.client.tcp.time")
-    @patch("pymodbus.client.tcp.select")
+    @mock.patch("pymodbus.client.tcp.time")
+    @mock.patch("pymodbus.client.tcp.select")
     def test_tcp_client_recv(self, mock_select, mock_time):
         """Test the tcp client receive method"""
         mock_select.select.return_value = [True]
         mock_time.time.side_effect = count()
         client = ModbusTcpClient("127.0.0.1")
-        self.assertRaises(
-            ConnectionException,
-            lambda: client.recv(1024),
-        )
+        with pytest.raises(ConnectionException):
+            client.recv(1024)
         client.socket = mockSocket()
-        self.assertEqual(b"", client.recv(0))
-        client.socket.mock_store(b"\x00" * 4)
-        self.assertEqual(b"\x00" * 4, client.recv(4))
+        assert client.recv(0) == b""
+        client.socket.mock_prepare_receive(b"\x00" * 4)
+        assert client.recv(4) == b"\x00" * 4
 
-        mock_socket = MagicMock()
+        mock_socket = mock.MagicMock()
         mock_socket.recv.side_effect = iter([b"\x00", b"\x01", b"\x02"])
         client.socket = mock_socket
         client.params.timeout = 3
-        self.assertEqual(b"\x00\x01\x02", client.recv(3))
+        assert client.recv(3) == b"\x00\x01\x02"
         mock_socket.recv.side_effect = iter([b"\x00", b"\x01", b"\x02"])
-        self.assertEqual(b"\x00\x01", client.recv(2))
+        assert client.recv(2) == b"\x00\x01"
         mock_select.select.return_value = [False]
-        self.assertEqual(b"", client.recv(2))
+        assert client.recv(2) == b""
         client.socket = mockSocket()
-        client.socket.mock_store(b"\x00")
+        client.socket.mock_prepare_receive(b"\x00")
         mock_select.select.return_value = [True]
-        self.assertIn(b"\x00", client.recv(None))
+        assert client.recv(None) in b"\x00"
 
-        mock_socket = MagicMock()
+        mock_socket = mock.MagicMock()
         mock_socket.recv.return_value = b""
         client.socket = mock_socket
-        self.assertRaises(
-            ConnectionException,
-            lambda: client.recv(1024),
-        )
-
+        with pytest.raises(ConnectionException):
+            client.recv(1024)
         mock_socket.recv.side_effect = iter([b"\x00", b"\x01", b"\x02", b""])
         client.socket = mock_socket
-        self.assertEqual(b"\x00\x01\x02", client.recv(1024))
+        assert client.recv(1024) == b"\x00\x01\x02"
 
     def test_tcp_client_repr(self):
         """Test tcp client."""
@@ -192,7 +189,7 @@ def test_tcp_client_repr(self):
             f"<{client.__class__.__name__} at {hex(id(client))} socket={client.socket}, "
             f"ipaddr={client.params.host}, port={client.params.port}, timeout={client.params.timeout}>"
         )
-        self.assertEqual(repr(client), rep)
+        assert repr(client) == rep
 
     def test_tcp_client_register(self):
         """Test tcp client."""
@@ -203,9 +200,9 @@ class CustomRequest:  # pylint: disable=too-few-public-methods
             function_code = 79
 
         client = ModbusTcpClient("127.0.0.1")
-        client.framer = Mock()
+        client.framer = mock.Mock()
         client.register(CustomRequest)
-        self.assertTrue(client.framer.decoder.register.called_once_with(CustomRequest))
+        assert client.framer.decoder.register.called_once_with(CustomRequest)
 
     # -----------------------------------------------------------------------#
     # Test TLS Client
@@ -213,104 +210,87 @@ class CustomRequest:  # pylint: disable=too-few-public-methods
 
     def test_tls_sslctx_provider(self):
         """Test that sslctx_provider() produce SSLContext correctly"""
-        with patch.object(ssl.SSLContext, "load_cert_chain") as mock_method:
+        with mock.patch.object(ssl.SSLContext, "load_cert_chain") as mock_method:
             sslctx1 = sslctx_provider(certfile="cert.pem")
-            self.assertIsNotNone(sslctx1)
-            self.assertEqual(type(sslctx1), ssl.SSLContext)
-            self.assertEqual(mock_method.called, False)
+            assert sslctx1
+            assert isinstance(sslctx1, ssl.SSLContext)
+            assert not mock_method.called
 
             sslctx2 = sslctx_provider(keyfile="key.pem")
-            self.assertIsNotNone(sslctx2)
-            self.assertEqual(type(sslctx2), ssl.SSLContext)
-            self.assertEqual(mock_method.called, False)
+            assert sslctx2
+            assert isinstance(sslctx2, ssl.SSLContext)
+            assert not mock_method.called
 
             sslctx3 = sslctx_provider(certfile="cert.pem", keyfile="key.pem")
-            self.assertIsNotNone(sslctx3)
-            self.assertEqual(type(sslctx3), ssl.SSLContext)
-            self.assertEqual(mock_method.called, True)
+            assert sslctx3
+            assert isinstance(sslctx3, ssl.SSLContext)
+            assert mock_method.called
 
             sslctx_old = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
             sslctx_new = sslctx_provider(sslctx=sslctx_old)
-            self.assertEqual(sslctx_new, sslctx_old)
+            assert sslctx_new == sslctx_old
 
     def test_syn_tls_client_instantiation(self):
         """Test sync tls client."""
         # default SSLContext
         client = ModbusTlsClient("127.0.0.1")
-        self.assertNotEqual(client, None)
-        self.assertIsInstance(client.framer, ModbusTlsFramer)
-        self.assertTrue(client.sslctx)
+        assert client
+        assert isinstance(client.framer, ModbusTlsFramer)
+        assert client.sslctx
 
-    @patch("pymodbus.client.tcp.select")
+    @mock.patch("pymodbus.client.tcp.select")
     def test_basic_syn_tls_client(self, mock_select):
         """Test the basic methods for the tls sync client"""
         # receive/send
         mock_select.select.return_value = [True]
         client = ModbusTlsClient("localhost")
         client.socket = mockSocket()
-        self.assertEqual(0, client.send(None))
-        self.assertEqual(1, client.send(b"\x45"))
-        self.assertEqual(b"\x45", client.recv(1))
+        assert not client.send(None)
+        assert client.send(b"\x45") == 1
+        assert client.recv(1) == b"\x45"
 
         # connect/disconnect
-        self.assertTrue(client.connect())
+        assert client.connect()
         client.close()
 
         # already closed socket
         client.socket = False
         client.close()
-        self.assertEqual("ModbusTlsClient(localhost:802)", str(client))
+        assert str(client) == "ModbusTlsClient(localhost:802)"
 
         client = ModbusTcpClient("127.0.0.1")
         client.socket = mockSocket()
-        self.assertEqual(0, client.send(None))
-        self.assertEqual(1, client.send(b"\x45"))
-        self.assertEqual(b"\x45", client.recv(1))
+        assert not client.send(None)
+        assert client.send(b"\x45") == 1
+        assert client.recv(1) == b"\x45"
 
     def test_tls_client_send(self):
         """Test the tls client send method"""
         client = ModbusTlsClient("127.0.0.1")
-        self.assertRaises(
-            ConnectionException,
-            lambda: client.send(None),
-        )
-
+        with pytest.raises(ConnectionException):
+            client.send(None)
         client.socket = mockSocket()
-        self.assertEqual(0, client.send(None))
-        self.assertEqual(4, client.send("1234"))
+        assert not client.send(None)
+        assert client.send("1234") == 4
 
-    @patch("pymodbus.client.tcp.time")
-    @patch("pymodbus.client.tcp.select")
+    @mock.patch("pymodbus.client.tcp.time")
+    @mock.patch("pymodbus.client.tcp.select")
     def test_tls_client_recv(self, mock_select, mock_time):
         """Test the tls client receive method"""
         mock_select.select.return_value = [True]
         client = ModbusTlsClient("127.0.0.1")
-        self.assertRaises(
-            ConnectionException,
-            lambda: client.recv(1024),
-        )
-
+        with pytest.raises(ConnectionException):
+            client.recv(1024)
         mock_time.time.side_effect = count()
 
         client.socket = mockSocket()
-        client.socket.mock_store(b"\x00" * 4)
-        self.assertEqual(b"", client.recv(0))
-        self.assertEqual(b"\x00" * 4, client.recv(4))
+        client.socket.mock_prepare_receive(b"\x00" * 4)
+        assert client.recv(0) == b""
+        assert client.recv(4) == b"\x00" * 4
 
         client.params.timeout = 2
-        client.socket.mock_store(b"\x00")
-        self.assertIn(b"\x00", client.recv(None))
-
-        # client.socket = mockSocket()
-        # client.socket.recv.side_effect = iter([b"\x00", b"\x01", b"\x02"])
-        # client.params.timeout = 3
-        # self.assertEqual(
-        #     b"\x00\x01\x02", client.recv(3)
-        # )
-        # client.socket.recv.side_effect = iter([b"\x00", b"\x01", b"\x02"])
-        # self.assertEqual(
-        #     b"\x00\x01", client.recv(2)
-        # )
+        client.socket.mock_prepare_receive(b"\x00")
+        assert b"\x00" in client.recv(None)
 
     def test_tls_client_repr(self):
         """Test tls client."""
@@ -320,7 +300,7 @@ def test_tls_client_repr(self):
             f"ipaddr={client.params.host}, port={client.params.port}, sslctx={client.sslctx}, "
             f"timeout={client.params.timeout}>"
         )
-        self.assertEqual(repr(client), rep)
+        assert repr(client) == rep
 
     def test_tls_client_register(self):
         """Test tls client."""
@@ -331,9 +311,9 @@ class CustomeRequest:  # pylint: disable=too-few-public-methods
             function_code = 79
 
         client = ModbusTlsClient("127.0.0.1")
-        client.framer = Mock()
+        client.framer = mock.Mock()
         client.register(CustomeRequest)
-        self.assertTrue(client.framer.decoder.register.called_once_with(CustomeRequest))
+        assert client.framer.decoder.register.called_once_with(CustomeRequest)
 
     # -----------------------------------------------------------------------#
     # Test Serial Client
@@ -341,40 +321,32 @@ class CustomeRequest:  # pylint: disable=too-few-public-methods
     def test_sync_serial_client_instantiation(self):
         """Test sync serial client."""
         client = ModbusSerialClient("/dev/null")
-        self.assertNotEqual(client, None)
-        self.assertTrue(
-            isinstance(
-                ModbusSerialClient("/dev/null", framer=ModbusAsciiFramer).framer,
-                ModbusAsciiFramer,
-            )
+        assert client
+        assert isinstance(
+            ModbusSerialClient("/dev/null", framer=ModbusAsciiFramer).framer,
+            ModbusAsciiFramer,
         )
-        self.assertTrue(
-            isinstance(
-                ModbusSerialClient("/dev/null", framer=ModbusRtuFramer).framer,
-                ModbusRtuFramer,
-            )
+        assert isinstance(
+            ModbusSerialClient("/dev/null", framer=ModbusRtuFramer).framer,
+            ModbusRtuFramer,
         )
-        self.assertTrue(
-            isinstance(
-                ModbusSerialClient("/dev/null", framer=ModbusBinaryFramer).framer,
-                ModbusBinaryFramer,
-            )
+        assert isinstance(
+            ModbusSerialClient("/dev/null", framer=ModbusBinaryFramer).framer,
+            ModbusBinaryFramer,
         )
-        self.assertTrue(
-            isinstance(
-                ModbusSerialClient("/dev/null", framer=ModbusSocketFramer).framer,
-                ModbusSocketFramer,
-            )
+        assert isinstance(
+            ModbusSerialClient("/dev/null", framer=ModbusSocketFramer).framer,
+            ModbusSocketFramer,
         )
 
     def test_sync_serial_rtu_client_timeouts(self):
         """Test sync serial rtu."""
         client = ModbusSerialClient("/dev/null", framer=ModbusRtuFramer, baudrate=9600)
-        self.assertEqual(client.silent_interval, round((3.5 * 11 / 9600), 6))
+        assert client.silent_interval == round((3.5 * 11 / 9600), 6)
         client = ModbusSerialClient("/dev/null", framer=ModbusRtuFramer, baudrate=38400)
-        self.assertEqual(client.silent_interval, round((1.75 / 1000), 6))
+        assert client.silent_interval == round((1.75 / 1000), 6)
 
-    @patch("serial.Serial")
+    @mock.patch("serial.Serial")
     def test_basic_sync_serial_client(self, mock_serial):
         """Test the basic methods for the serial sync client."""
         # receive/send
@@ -385,25 +357,23 @@ def test_basic_sync_serial_client(self, mock_serial):
         client = ModbusSerialClient("/dev/null")
         client.socket = mock_serial
         client.state = 0
-        self.assertEqual(0, client.send(None))
+        assert not client.send(None)
         client.state = 0
-        self.assertEqual(1, client.send(b"\x00"))
-        self.assertEqual(b"\x00", client.recv(1))
+        assert client.send(b"\x00") == 1
+        assert client.recv(1) == b"\x00"
 
         # connect/disconnect
-        self.assertTrue(client.connect())
+        assert client.connect()
         client.close()
 
         # rtu connect/disconnect
         rtu_client = ModbusSerialClient(
             "/dev/null", framer=ModbusRtuFramer, strict=True
         )
-        self.assertTrue(rtu_client.connect())
-        self.assertEqual(
-            rtu_client.socket.interCharTimeout, rtu_client.inter_char_timeout
-        )
+        assert rtu_client.connect()
+        assert rtu_client.socket.interCharTimeout == rtu_client.inter_char_timeout
         rtu_client.close()
-        self.assertTrue("baud[19200])" in str(client))
+        assert "baud[19200])" in str(client)
 
         # already closed socket
         client.socket = False
@@ -411,76 +381,67 @@ def test_basic_sync_serial_client(self, mock_serial):
 
     def test_serial_client_connect(self):
         """Test the serial client connection method"""
-        with patch.object(serial, "Serial") as mock_method:
-            mock_method.return_value = MagicMock()
+        with mock.patch.object(serial, "Serial") as mock_method:
+            mock_method.return_value = mock.MagicMock()
             client = ModbusSerialClient("/dev/null")
-            self.assertTrue(client.connect())
+            assert client.connect()
 
-        with patch.object(serial, "Serial") as mock_method:
+        with mock.patch.object(serial, "Serial") as mock_method:
             mock_method.side_effect = serial.SerialException()
             client = ModbusSerialClient("/dev/null")
-            self.assertFalse(client.connect())
+            assert not client.connect()
 
-    @patch("serial.Serial")
+    @mock.patch("serial.Serial")
     def test_serial_client_is_socket_open(self, mock_serial):
         """Test the serial client is_socket_open method"""
         client = ModbusSerialClient("/dev/null")
-        self.assertFalse(client.is_socket_open())
+        assert not client.is_socket_open()
         client.socket = mock_serial
-        self.assertTrue(client.is_socket_open())
+        assert client.is_socket_open()
 
-    @patch("serial.Serial")
+    @mock.patch("serial.Serial")
     def test_serial_client_send(self, mock_serial):
         """Test the serial client send method"""
         mock_serial.in_waiting = None
         mock_serial.write = lambda x: len(x)  # pylint: disable=unnecessary-lambda
         client = ModbusSerialClient("/dev/null")
-        self.assertRaises(
-            ConnectionException,
-            lambda: client.send(None),
-        )
-        # client.connect()
+        with pytest.raises(ConnectionException):
+            client.send(None)
         client.socket = mock_serial
         client.state = 0
-        self.assertEqual(0, client.send(None))
+        assert not client.send(None)
         client.state = 0
-        self.assertEqual(4, client.send("1234"))
+        assert client.send("1234") == 4
 
-    @patch("serial.Serial")
+    @mock.patch("serial.Serial")
     def test_serial_client_cleanup_buffer_before_send(self, mock_serial):
         """Test the serial client send method"""
         mock_serial.in_waiting = 4
         mock_serial.read = lambda x: b"1" * x
         mock_serial.write = lambda x: len(x)  # pylint: disable=unnecessary-lambda
         client = ModbusSerialClient("/dev/null")
-        self.assertRaises(
-            ConnectionException,
-            lambda: client.send(None),
-        )
-        # client.connect()
+        with pytest.raises(ConnectionException):
+            client.send(None)
         client.socket = mock_serial
         client.state = 0
-        self.assertEqual(0, client.send(None))
+        assert not client.send(None)
         client.state = 0
-        self.assertEqual(4, client.send("1234"))
+        assert client.send("1234") == 4
 
     def test_serial_client_recv(self):
         """Test the serial client receive method"""
         client = ModbusSerialClient("/dev/null")
-        self.assertRaises(
-            ConnectionException,
-            lambda: client.recv(1024),
-        )
-
+        with pytest.raises(ConnectionException):
+            client.recv(1024)
         client.socket = mockSocket()
-        self.assertEqual(b"", client.recv(0))
-        client.socket.mock_store(b"\x00" * 4)
-        self.assertEqual(b"\x00" * 4, client.recv(4))
+        assert client.recv(0) == b""
+        client.socket.mock_prepare_receive(b"\x00" * 4)
+        assert client.recv(4) == b"\x00" * 4
         client.socket = mockSocket()
-        client.socket.mock_store(b"")
-        self.assertEqual(b"", client.recv(None))
+        client.socket.mock_prepare_receive(b"")
+        assert client.recv(None) == b""
         client.socket.timeout = 0
-        self.assertEqual(b"", client.recv(0))
+        assert client.recv(0) == b""
 
     def test_serial_client_repr(self):
         """Test serial client."""
@@ -489,4 +450,4 @@ def test_serial_client_repr(self):
             f"<{client.__class__.__name__} at {hex(id(client))} socket={client.socket}, "
             f"framer={client.framer}, timeout={client.params.timeout}>"
         )
-        self.assertEqual(repr(client), rep)
+        assert repr(client) == rep
diff --git a/test/test_client_sync_diag.py b/test/test_client_sync_diag.py
deleted file mode 100755
index 69d8508f3..000000000
--- a/test/test_client_sync_diag.py
+++ /dev/null
@@ -1,114 +0,0 @@
-"""Test client sync diag."""
-import socket
-import unittest
-from itertools import count
-from test.test_client_sync import mockSocket
-from unittest.mock import MagicMock, patch
-
-from pymodbus.client.sync_diag import ModbusTcpDiagClient, get_client
-from pymodbus.exceptions import ConnectionException
-
-
-# ---------------------------------------------------------------------------#
-# Fixture
-# ---------------------------------------------------------------------------#
-
-
-class SynchronousDiagnosticClientTest(unittest.TestCase):
-    """Unittest for the pymodbus.client.sync_diag module.
-
-    It is a copy of parts of the test for the TCP class in the pymodbus.client
-    module, as it should operate identically and only log some additional
-    lines.
-    """
-
-    # -----------------------------------------------------------------------#
-    # Test TCP Diagnostic Client
-    # -----------------------------------------------------------------------#
-
-    def test_syn_tcp_diag_client_instantiation(self):
-        """Test sync tcp diag client."""
-        client = get_client()
-        self.assertNotEqual(client, None)
-
-    def test_basic_syn_tcp_diag_client(self):
-        """Test the basic methods for the tcp sync diag client"""
-        # connect/disconnect
-        client = ModbusTcpDiagClient()
-        client.socket = mockSocket()
-        self.assertTrue(client.connect())
-        client.close()
-
-    def test_tcp_diag_client_connect(self):
-        """Test the tcp sync diag client connection method"""
-        with patch.object(socket, "create_connection") as mock_method:
-            mock_method.return_value = object()
-            client = ModbusTcpDiagClient()
-            self.assertTrue(client.connect())
-
-        with patch.object(socket, "create_connection") as mock_method:
-            mock_method.side_effect = socket.error()
-            client = ModbusTcpDiagClient()
-            self.assertFalse(client.connect())
-
-    @patch("pymodbus.client.tcp.time")
-    @patch("pymodbus.client.sync_diag.time")
-    @patch("pymodbus.client.tcp.select")
-    def test_tcp_diag_client_recv(self, mock_select, mock_diag_time, mock_time):
-        """Test the tcp sync diag client receive method"""
-        mock_select.select.return_value = [True]
-        mock_time.time.side_effect = count()
-        mock_diag_time.time.side_effect = count()
-        client = ModbusTcpDiagClient()
-        self.assertRaises(
-            ConnectionException,
-            lambda: client.recv(1024),
-        )
-
-        client.socket = mockSocket()
-        # Test logging of non-delayed responses
-        client.socket.mock_store(b"\x00")
-        self.assertIn(b"\x00", client.recv(None))
-        client.socket = mockSocket()
-        client.socket.mock_store(b"\x00")
-        self.assertEqual(b"\x00", client.recv(1))
-
-        # Fool diagnostic logger into thinking we"re running late,
-        # test logging of delayed responses
-        mock_diag_time.time.side_effect = count(step=3)
-        client.socket.mock_store(b"\x00" * 4)
-        self.assertEqual(b"\x00" * 4, client.recv(4))
-        self.assertEqual(b"", client.recv(0))
-
-        client.socket.mock_store(b"\x00\x01\x02")
-        client.timeout = 3
-        self.assertEqual(b"\x00\x01\x02", client.recv(3))
-        client.socket.mock_store(b"\x00\x01\x02")
-        self.assertEqual(b"\x00\x01", client.recv(2))
-        mock_select.select.return_value = [False]
-        self.assertEqual(b"", client.recv(2))
-        client.socket = mockSocket()
-        client.socket.mock_store(b"\x00")
-        mock_select.select.return_value = [True]
-        self.assertIn(b"\x00", client.recv(None))
-
-        mock_socket = MagicMock()
-        client.socket = mock_socket
-        mock_socket.recv.return_value = b""
-        self.assertRaises(
-            ConnectionException,
-            lambda: client.recv(1024),
-        )
-        client.socket = mockSocket()
-        client.socket.mock_store(b"\x00\x01\x02")
-        self.assertEqual(b"\x00\x01\x02", client.recv(1024))
-
-    def test_tcp_diag_client_repr(self):
-        """Test tcp diag client."""
-        client = ModbusTcpDiagClient()
-        rep = (
-            f"<{client.__class__.__name__} at {hex(id(client))} "
-            f"socket={client.socket}, ipaddr={client.params.host}, "
-            f"port={client.params.port}, timeout={client.params.timeout}>"
-        )
-        self.assertEqual(repr(client), rep)
diff --git a/test/test_datastore.py b/test/test_datastore.py
deleted file mode 100644
index c1bf26c48..000000000
--- a/test/test_datastore.py
+++ /dev/null
@@ -1,504 +0,0 @@
-"""Test datastore."""
-import random
-import unittest
-from unittest.mock import MagicMock
-
-import pytest
-import redis
-
-from pymodbus.datastore import (
-    ModbusSequentialDataBlock,
-    ModbusServerContext,
-    ModbusSlaveContext,
-    ModbusSparseDataBlock,
-)
-from pymodbus.datastore.database import RedisSlaveContext, SqlSlaveContext
-from pymodbus.datastore.store import BaseModbusDataBlock
-from pymodbus.exceptions import (
-    NoSuchSlaveException,
-    NotImplementedException,
-    ParameterException,
-)
-
-
-class ModbusDataStoreTest(unittest.TestCase):
-    """Unittest for the pymodbus.datastore module."""
-
-    def setUp(self):
-        """Do setup."""
-
-    def tearDown(self):
-        """Clean up the test environment"""
-
-    def test_modbus_data_block(self):
-        """Test a base data block store"""
-        block = BaseModbusDataBlock()
-        block.default(10, True)
-
-        self.assertNotEqual(str(block), None)
-        self.assertEqual(block.default_value, True)
-        self.assertEqual(block.values, [True] * 10)
-
-        block.default_value = False
-        block.reset()
-        self.assertEqual(block.values, [False] * 10)
-
-    def test_modbus_data_block_iterate(self):
-        """Test a base data block store"""
-        block = BaseModbusDataBlock()
-        block.default(10, False)
-        for _, value in block:
-            self.assertEqual(value, False)
-
-        block.values = {0: False, 2: False, 3: False}
-        for _, value in block:
-            self.assertEqual(value, False)
-
-    def test_modbus_data_block_other(self):
-        """Test a base data block store"""
-        block = BaseModbusDataBlock()
-        self.assertRaises(NotImplementedException, lambda: block.validate(1, 1))
-        self.assertRaises(NotImplementedException, lambda: block.getValues(1, 1))
-        self.assertRaises(NotImplementedException, lambda: block.setValues(1, 1))
-
-    def test_modbus_sequential_data_block(self):
-        """Test a sequential data block store"""
-        block = ModbusSequentialDataBlock(0x00, [False] * 10)
-        self.assertFalse(block.validate(-1, 0))
-        self.assertFalse(block.validate(0, 20))
-        self.assertFalse(block.validate(10, 1))
-        self.assertTrue(block.validate(0x00, 10))
-
-        block.setValues(0x00, True)
-        self.assertEqual(block.getValues(0x00, 1), [True])
-
-        block.setValues(0x00, [True] * 10)
-        self.assertEqual(block.getValues(0x00, 10), [True] * 10)
-
-    def test_modbus_sequential_data_block_factory(self):
-        """Test the sequential data block store factory"""
-        block = ModbusSequentialDataBlock.create()
-        self.assertEqual(block.getValues(0x00, 65536), [False] * 65536)
-        block = ModbusSequentialDataBlock(0x00, 0x01)
-        self.assertEqual(block.values, [0x01])
-
-    def test_modbus_sparse_data_block(self):
-        """Test a sparse data block store"""
-        values = dict(enumerate([True] * 10))
-        block = ModbusSparseDataBlock(values)
-        self.assertFalse(block.validate(-1, 0))
-        self.assertFalse(block.validate(0, 20))
-        self.assertFalse(block.validate(10, 1))
-        self.assertTrue(block.validate(0x00, 10))
-        self.assertTrue(block.validate(0x00, 10))
-        self.assertFalse(block.validate(0, 0))
-        self.assertFalse(block.validate(5, 0))
-
-        block.setValues(0x00, True)
-        self.assertEqual(block.getValues(0x00, 1), [True])
-
-        block.setValues(0x00, [True] * 10)
-        self.assertEqual(block.getValues(0x00, 10), [True] * 10)
-
-        block.setValues(0x00, dict(enumerate([False] * 10)))
-        self.assertEqual(block.getValues(0x00, 10), [False] * 10)
-
-        block = ModbusSparseDataBlock({3: [10, 11, 12], 10: 1, 15: [0] * 4})
-        self.assertEqual(
-            block.values, {3: 10, 4: 11, 5: 12, 10: 1, 15: 0, 16: 0, 17: 0, 18: 0}
-        )
-        self.assertEqual(
-            block.default_value,
-            {3: 10, 4: 11, 5: 12, 10: 1, 15: 0, 16: 0, 17: 0, 18: 0},
-        )
-        self.assertEqual(block.mutable, True)
-        block.setValues(3, [20, 21, 22, 23], use_as_default=True)
-        self.assertEqual(block.getValues(3, 4), [20, 21, 22, 23])
-        self.assertEqual(
-            block.default_value,
-            {3: 20, 4: 21, 5: 22, 6: 23, 10: 1, 15: 0, 16: 0, 17: 0, 18: 0},
-        )
-        # check when values is a dict, address is ignored
-        block.setValues(0, {5: 32, 7: 43})
-        self.assertEqual(block.getValues(5, 3), [32, 23, 43])
-
-        # assertEqual value is empty dict when initialized without params
-        block = ModbusSparseDataBlock()
-        self.assertEqual(block.values, {})
-
-        # mark block as unmutable and see if parameter exception
-        # is raised for invalid offset writes
-        block = ModbusSparseDataBlock({1: 100}, mutable=False)
-        self.assertRaises(ParameterException, block.setValues, 0, 1)
-        self.assertRaises(ParameterException, block.setValues, 0, {2: 100})
-        self.assertRaises(ParameterException, block.setValues, 0, [1] * 10)
-
-        # Reset datablock
-        block = ModbusSparseDataBlock({3: [10, 11, 12], 10: 1, 15: [0] * 4})
-        block.setValues(0, {3: [20, 21, 22], 10: 11, 15: [10] * 4})
-        self.assertEqual(
-            block.values, {3: 20, 4: 21, 5: 22, 10: 11, 15: 10, 16: 10, 17: 10, 18: 10}
-        )
-        block.reset()
-        self.assertEqual(
-            block.values, {3: 10, 4: 11, 5: 12, 10: 1, 15: 0, 16: 0, 17: 0, 18: 0}
-        )
-
-    def test_modbus_sparse_data_block_factory(self):
-        """Test the sparse data block store factory"""
-        block = ModbusSparseDataBlock.create([0x00] * 65536)
-        self.assertEqual(block.getValues(0x00, 65536), [False] * 65536)
-
-    def test_modbus_sparse_data_block_other(self):
-        """Test modbus sparce data block."""
-        block = ModbusSparseDataBlock([True] * 10)
-        self.assertEqual(block.getValues(0x00, 10), [True] * 10)
-        self.assertRaises(ParameterException, lambda: ModbusSparseDataBlock(True))
-
-    def test_modbus_slave_context(self):
-        """Test a modbus slave context"""
-        store = {
-            "di": ModbusSequentialDataBlock(0, [False] * 10),
-            "co": ModbusSequentialDataBlock(0, [False] * 10),
-            "ir": ModbusSequentialDataBlock(0, [False] * 10),
-            "hr": ModbusSequentialDataBlock(0, [False] * 10),
-        }
-        context = ModbusSlaveContext(**store)
-        self.assertNotEqual(str(context), None)
-
-        for i in (1, 2, 3, 4):
-            context.setValues(i, 0, [True] * 10)
-            self.assertTrue(context.validate(i, 0, 10))
-            self.assertEqual(context.getValues(i, 0, 10), [True] * 10)
-        context.reset()
-
-        for i in (1, 2, 3, 4):
-            self.assertTrue(context.validate(i, 0, 10))
-            self.assertEqual(context.getValues(i, 0, 10), [False] * 10)
-
-    def test_modbus_server_context(self):
-        """Test a modbus server context"""
-
-        def _set(ctx):
-            ctx[0xFFFF] = None
-
-        context = ModbusServerContext(single=False)
-        self.assertRaises(NoSuchSlaveException, lambda: _set(context))
-        self.assertRaises(NoSuchSlaveException, lambda: context[0xFFFF])
-
-
-class RedisDataStoreTest(unittest.TestCase):
-    """Unittest for the pymodbus.datastore.database.redis module."""
-
-    def setUp(self):
-        """Do setup."""
-        self.slave = RedisSlaveContext()
-
-    def tearDown(self):
-        """Clean up the test environment"""
-
-    def test_str(self):
-        """Test string."""
-        # slave = RedisSlaveContext()
-        self.assertEqual(str(self.slave), f"Redis Slave Context {self.slave.client}")
-
-    def test_reset(self):
-        """Test reset."""
-        self.assertTrue(isinstance(self.slave.client, redis.Redis))
-        self.slave.client = MagicMock()
-        self.slave.reset()
-        self.slave.client.flushall.assert_called_once_with()
-
-    def test_val_callbacks_success(self):
-        """Test value callbacks success."""
-        self.slave._build_mapping()  # pylint: disable=protected-access
-        mock_count = 3
-        mock_offset = 0
-        self.slave.client.mset = MagicMock()
-        self.slave.client.mget = MagicMock(return_value=["11"])
-
-        for key in ("d", "c", "h", "i"):
-            self.assertTrue(
-                self.slave._val_callbacks[key](  # pylint: disable=protected-access
-                    mock_offset, mock_count
-                )
-            )
-
-    def test_val_callbacks_failure(self):
-        """Test value callbacks failure."""
-        self.slave._build_mapping()  # pylint: disable=protected-access
-        mock_count = 3
-        mock_offset = 0
-        self.slave.client.mset = MagicMock()
-        self.slave.client.mget = MagicMock(return_value=["11", None])
-
-        for key in ("d", "c", "h", "i"):
-            self.assertFalse(
-                self.slave._val_callbacks[key](  # pylint: disable=protected-access
-                    mock_offset, mock_count
-                )
-            )
-
-    def test_get_callbacks(self):
-        """Test get callbacks."""
-        self.slave._build_mapping()  # pylint: disable=protected-access
-        mock_count = 3
-        mock_offset = 0
-        self.slave.client.mget = MagicMock(return_value="11")
-
-        for key in ("d", "c"):
-            resp = self.slave._get_callbacks[key](  # pylint: disable=protected-access
-                mock_offset, mock_count
-            )
-            self.assertEqual(resp, [True, False, False])
-
-        for key in ("h", "i"):
-            resp = self.slave._get_callbacks[key](  # pylint: disable=protected-access
-                mock_offset, mock_count
-            )
-            self.assertEqual(resp, ["1", "1"])
-
-    def test_set_callbacks(self):
-        """Test set callbacks."""
-        self.slave._build_mapping()  # pylint: disable=protected-access
-        mock_values = [3]
-        mock_offset = 0
-        self.slave.client.mset = MagicMock()
-        self.slave.client.mget = MagicMock()
-
-        for key in ("c", "d"):
-            self.slave._set_callbacks[key](  # pylint: disable=protected-access
-                mock_offset, [3]
-            )
-            k = f"pymodbus:{key}:{mock_offset}"
-            self.slave.client.mset.assert_called_with({k: "\x01"})
-
-        for key in ("h", "i"):
-            self.slave._set_callbacks[key](  # pylint: disable=protected-access
-                mock_offset, [3]
-            )
-            k = f"pymodbus:{key}:{mock_offset}"
-            self.slave.client.mset.assert_called_with({k: mock_values[0]})
-
-    def test_validate(self):
-        """Test validate."""
-        self.slave.client.mget = MagicMock(return_value=[123])
-        self.assertTrue(self.slave.validate(0x01, 3000))
-
-    def test_set_value(self):
-        """Test set value."""
-        self.slave.client.mset = MagicMock()
-        self.slave.client.mget = MagicMock()
-        self.assertEqual(self.slave.setValues(0x01, 1000, [12]), None)
-
-    def test_get_value(self):
-        """Test get value."""
-        self.slave.client.mget = MagicMock(return_value=["123"])
-        self.assertEqual(self.slave.getValues(0x01, 23), [])
-
-
-class MockSqlResult:  # pylint: disable=too-few-public-methods
-    """Mock SQL Result."""
-
-    def __init__(self, rowcount=0, value=0):
-        """Initialize."""
-        self.rowcount = rowcount
-        self.value = value
-
-
-class SqlDataStoreTest(unittest.TestCase):
-    """Unittest for the pymodbus.datastore.database.SqlSlaveContext module."""
-
-    class SQLunit:  # pylint: disable=too-few-public-methods
-        """Single test setup."""
-
-        def __init__(self):
-            """Prepare test."""
-            self.slave = SqlSlaveContext()
-            self.slave._metadata.drop_all = MagicMock()
-            self.slave._db_create = MagicMock()
-            self.slave._table.select = MagicMock()
-            self.slave._connection = MagicMock()
-
-            self.mock_addr = random.randint(0, 65000)
-            self.mock_values = random.sample(range(1, 100), 5)
-            self.mock_function = 0x01
-            self.mock_type = "h"
-            self.mock_offset = 0
-            self.mock_count = 1
-
-            self.function_map = {2: "d", 4: "i"}
-            self.function_map.update([(i, "h") for i in (3, 6, 16, 22, 23)])
-            self.function_map.update([(i, "c") for i in (1, 5, 15)])
-
-    def setUp(self):
-        """Do setup."""
-
-    def tearDown(self):
-        """Clean up the test environment"""
-
-    @pytest.mark.skip
-    @pytest.mark.xdist_group(name="sql")
-    def test_str(self):
-        """Test string."""
-        unit = self.SQLunit()
-        self.assertEqual(str(unit.slave), "Modbus Slave Context")
-
-    @pytest.mark.skip
-    @pytest.mark.xdist_group(name="sql")
-    def test_reset(self):
-        """Test reset."""
-        unit = self.SQLunit()
-        unit.slave.reset()
-
-        unit.slave._metadata.drop_all.assert_called_once_with()  # pylint: disable=protected-access
-        unit.slave._db_create.assert_called_once_with(  # pylint: disable=protected-access
-            unit.slave.table, unit.slave.database
-        )
-
-    @pytest.mark.skip
-    @pytest.mark.xdist_group(name="sql")
-    def test_validate_success(self):
-        """Test validate success."""
-        unit = self.SQLunit()
-        unit.slave._connection.execute.return_value.fetchall.return_value = (  # pylint: disable=protected-access
-            unit.mock_values
-        )
-        self.assertTrue(
-            unit.slave.validate(
-                unit.mock_function, unit.mock_addr, len(unit.mock_values)
-            )
-        )
-
-    @pytest.mark.skip
-    @pytest.mark.xdist_group(name="sql")
-    def test_validate_failure(self):
-        """Test validate failure."""
-        unit = self.SQLunit()
-        wrong_count = 9
-        unit.slave._connection.execute.return_value.fetchall.return_value = (  # pylint: disable=protected-access
-            unit.mock_values
-        )
-        self.assertFalse(
-            unit.slave.validate(unit.mock_function, unit.mock_addr, wrong_count)
-        )
-
-    @pytest.mark.skip
-    @pytest.mark.xdist_group(name="sql")
-    def test_build_set(self):
-        """Test build set."""
-        unit = self.SQLunit()
-        mock_set = [
-            {"index": 0, "type": "h", "value": 11},
-            {"index": 1, "type": "h", "value": 12},
-        ]
-        self.assertListEqual(
-            unit.slave._build_set("h", 0, [11, 12]),  # pylint: disable=protected-access
-            mock_set,
-        )
-
-    @pytest.mark.skip
-    @pytest.mark.xdist_group(name="sql")
-    def test_check_success(self):
-        """Test check success."""
-        unit = self.SQLunit()
-        mock_success_results = [1, 2, 3]
-        unit.slave._get = MagicMock(  # pylint: disable=protected-access
-            return_value=mock_success_results
-        )
-        self.assertFalse(
-            unit.slave._check("h", 0, 1)  # pylint: disable=protected-access
-        )
-
-    @pytest.mark.skip
-    @pytest.mark.xdist_group(name="sql")
-    def test_check_failure(self):
-        """Test check failure."""
-        unit = self.SQLunit()
-        mock_success_results = []
-        unit.slave._get = MagicMock(  # pylint: disable=protected-access
-            return_value=mock_success_results
-        )
-        self.assertTrue(
-            unit.slave._check("h", 0, 1)  # pylint: disable=protected-access
-        )
-
-    @pytest.mark.skip
-    @pytest.mark.xdist_group(name="sql")
-    def test_get_values(self):
-        """Test get values."""
-        unit = self.SQLunit()
-        unit.slave._get = MagicMock()  # pylint: disable=protected-access
-
-        for key, value in unit.function_map.items():
-            unit.slave.getValues(key, unit.mock_addr, unit.mock_count)
-            unit.slave._get.assert_called_with(  # pylint: disable=protected-access
-                value, unit.mock_addr + 1, unit.mock_count
-            )
-
-    @pytest.mark.skip
-    @pytest.mark.xdist_group(name="sql")
-    def test_set_values(self):
-        """Test set values."""
-        unit = self.SQLunit()
-        unit.slave._set = MagicMock()  # pylint: disable=protected-access
-
-        for key, value in unit.function_map.items():
-            unit.slave.setValues(key, unit.mock_addr, unit.mock_values, update=False)
-            unit.slave._set.assert_called_with(  # pylint: disable=protected-access
-                value, unit.mock_addr + 1, unit.mock_values
-            )
-
-    @pytest.mark.skip
-    @pytest.mark.xdist_group(name="sql")
-    def test_set(self):
-        """Test set."""
-        unit = self.SQLunit()
-        unit.slave._check = MagicMock(  # pylint: disable=protected-access
-            return_value=True
-        )
-        unit.slave._connection.execute = MagicMock(  # pylint: disable=protected-access
-            return_value=MockSqlResult(rowcount=len(unit.mock_values))
-        )
-        self.assertTrue(
-            unit.slave._set(  # pylint: disable=protected-access
-                unit.mock_type, unit.mock_offset, unit.mock_values
-            )
-        )
-
-        unit.slave._check = MagicMock(  # pylint: disable=protected-access
-            return_value=False
-        )
-        self.assertFalse(
-            unit.slave._set(  # pylint: disable=protected-access
-                unit.mock_type, unit.mock_offset, unit.mock_values
-            )
-        )
-
-    @pytest.mark.skip
-    @pytest.mark.xdist_group(name="sql")
-    def test_update_success(self):
-        """Test update success."""
-        unit = self.SQLunit()
-        unit.slave._connection.execute = MagicMock(  # pylint: disable=protected-access
-            return_value=MockSqlResult(rowcount=len(unit.mock_values))
-        )
-        self.assertTrue(
-            unit.slave._update(  # pylint: disable=protected-access
-                unit.mock_type, unit.mock_offset, unit.mock_values
-            )
-        )
-
-    @pytest.mark.skip
-    @pytest.mark.xdist_group(name="sql")
-    def test_update_failure(self):
-        """Test update failure."""
-        unit = self.SQLunit()
-        unit.slave._connection.execute = MagicMock(  # pylint: disable=protected-access
-            return_value=MockSqlResult(rowcount=100)
-        )
-        self.assertFalse(
-            unit.slave._update(  # pylint: disable=protected-access
-                unit.mock_type, unit.mock_offset, unit.mock_values
-            )
-        )
diff --git a/test/test_device.py b/test/test_device.py
index d32ab3506..970fb7ffd 100644
--- a/test/test_device.py
+++ b/test/test_device.py
@@ -1,6 +1,4 @@
 """Test device."""
-import unittest
-
 from pymodbus.constants import DeviceInformation
 from pymodbus.device import (
     DeviceInformationFactory,
@@ -16,14 +14,18 @@
 # ---------------------------------------------------------------------------#
 
 
-class SimpleDataStoreTest(unittest.TestCase):
+class TestDataStore:
     """Unittest for the pymodbus.device module."""
 
     # -----------------------------------------------------------------------#
     #  Setup/TearDown
     # -----------------------------------------------------------------------#
 
-    def setUp(self):
+    info = None
+    ident = None
+    control = None
+
+    def setup_method(self):
         """Do setup."""
         self.info = {
             0x00: "Bashwork",  # VendorName
@@ -32,7 +34,7 @@ def setUp(self):
             0x03: "http://internets.com",  # VendorUrl
             0x04: "pymodbus",  # ProductName
             0x05: "bashwork",  # ModelName
-            0x06: "unittest",  # UserApplicationName
+            0x06: "pytest",  # UserApplicationName
             0x07: "x",  # reserved
             0x08: "x",  # reserved
             0x10: "reserved",  # reserved
@@ -44,21 +46,16 @@ def setUp(self):
         self.control = ModbusControlBlock()
         self.control.reset()
 
-    def tearDown(self):
-        """Clean up the test environment"""
-        del self.ident
-        del self.control
-
     def test_update_identity(self):
         """Test device identification reading"""
         self.control.Identity.update(self.ident)
-        self.assertEqual(self.control.Identity.VendorName, "Bashwork")
-        self.assertEqual(self.control.Identity.ProductCode, "PTM")
-        self.assertEqual(self.control.Identity.MajorMinorRevision, "1.0")
-        self.assertEqual(self.control.Identity.VendorUrl, "http://internets.com")
-        self.assertEqual(self.control.Identity.ProductName, "pymodbus")
-        self.assertEqual(self.control.Identity.ModelName, "bashwork")
-        self.assertEqual(self.control.Identity.UserApplicationName, "unittest")
+        assert self.control.Identity.VendorName == "Bashwork"
+        assert self.control.Identity.ProductCode == "PTM"
+        assert self.control.Identity.MajorMinorRevision == "1.0"
+        assert self.control.Identity.VendorUrl == "http://internets.com"
+        assert self.control.Identity.ProductName == "pymodbus"
+        assert self.control.Identity.ModelName == "bashwork"
+        assert self.control.Identity.UserApplicationName == "pytest"
 
     def test_device_identification_factory(self):
         """Test device identification reading"""
@@ -66,107 +63,119 @@ def test_device_identification_factory(self):
         result = DeviceInformationFactory.get(
             self.control, DeviceInformation.Specific, 0x00
         )
-        self.assertEqual(result[0x00], "Bashwork")
+        assert result[0x00] == "Bashwork"
 
         result = DeviceInformationFactory.get(
             self.control, DeviceInformation.Basic, 0x00
         )
-        self.assertEqual(result[0x00], "Bashwork")
-        self.assertEqual(result[0x01], "PTM")
-        self.assertEqual(result[0x02], "1.0")
+        assert result[0x00] == "Bashwork"
+        assert result[0x01] == "PTM"
+        assert result[0x02] == "1.0"
 
         result = DeviceInformationFactory.get(
             self.control, DeviceInformation.Regular, 0x00
         )
-        self.assertEqual(result[0x00], "Bashwork")
-        self.assertEqual(result[0x01], "PTM")
-        self.assertEqual(result[0x02], "1.0")
-        self.assertEqual(result[0x03], "http://internets.com")
-        self.assertEqual(result[0x04], "pymodbus")
-        self.assertEqual(result[0x05], "bashwork")
-        self.assertEqual(result[0x06], "unittest")
+        assert result[0x00] == "Bashwork"
+        assert result[0x01] == "PTM"
+        assert result[0x02] == "1.0"
+        assert result[0x03] == "http://internets.com"
+        assert result[0x04] == "pymodbus"
+        assert result[0x05] == "bashwork"
+        assert result[0x06] == "pytest"
 
     def test_device_identification_factory_lookup(self):
         """Test device identification factory lookup."""
         result = DeviceInformationFactory.get(
             self.control, DeviceInformation.Basic, 0x00
         )
-        self.assertEqual(sorted(result.keys()), [0x00, 0x01, 0x02])
+        assert sorted(result.keys()) == [0x00, 0x01, 0x02]
         result = DeviceInformationFactory.get(
             self.control, DeviceInformation.Basic, 0x02
         )
-        self.assertEqual(sorted(result.keys()), [0x02])
+        assert sorted(result.keys()) == [0x02]
         result = DeviceInformationFactory.get(
             self.control, DeviceInformation.Regular, 0x00
         )
-        self.assertEqual(
-            sorted(result.keys()), [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06]
-        )
+        assert sorted(result.keys()) == [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06]
         result = DeviceInformationFactory.get(
             self.control, DeviceInformation.Regular, 0x01
         )
-        self.assertEqual(sorted(result.keys()), [0x01, 0x02, 0x03, 0x04, 0x05, 0x06])
+        assert sorted(result.keys()) == [0x01, 0x02, 0x03, 0x04, 0x05, 0x06]
         result = DeviceInformationFactory.get(
             self.control, DeviceInformation.Regular, 0x05
         )
-        self.assertEqual(sorted(result.keys()), [0x05, 0x06])
+        assert sorted(result.keys()) == [0x05, 0x06]
         result = DeviceInformationFactory.get(
             self.control, DeviceInformation.Extended, 0x00
         )
-        self.assertEqual(
-            sorted(result.keys()),
-            [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x80, 0x82, 0xFF],
-        )
+        assert sorted(result.keys()) == [
+            0x00,
+            0x01,
+            0x02,
+            0x03,
+            0x04,
+            0x05,
+            0x06,
+            0x80,
+            0x82,
+            0xFF,
+        ]
         result = DeviceInformationFactory.get(
             self.control, DeviceInformation.Extended, 0x02
         )
-        self.assertEqual(
-            sorted(result.keys()), [0x02, 0x03, 0x04, 0x05, 0x06, 0x80, 0x82, 0xFF]
-        )
+        assert sorted(result.keys()) == [0x02, 0x03, 0x04, 0x05, 0x06, 0x80, 0x82, 0xFF]
         result = DeviceInformationFactory.get(
             self.control, DeviceInformation.Extended, 0x06
         )
-        self.assertEqual(sorted(result.keys()), [0x06, 0x80, 0x82, 0xFF])
+        assert sorted(result.keys()) == [0x06, 0x80, 0x82, 0xFF]
         result = DeviceInformationFactory.get(
             self.control, DeviceInformation.Extended, 0x80
         )
-        self.assertEqual(sorted(result.keys()), [0x80, 0x82, 0xFF])
+        assert sorted(result.keys()) == [0x80, 0x82, 0xFF]
         result = DeviceInformationFactory.get(
             self.control, DeviceInformation.Extended, 0x82
         )
-        self.assertEqual(sorted(result.keys()), [0x82, 0xFF])
+        assert sorted(result.keys()) == [0x82, 0xFF]
         result = DeviceInformationFactory.get(
             self.control, DeviceInformation.Extended, 0x81
         )
-        self.assertEqual(
-            sorted(result.keys()),
-            [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x80, 0x82, 0xFF],
-        )
+        assert sorted(result.keys()) == [
+            0x00,
+            0x01,
+            0x02,
+            0x03,
+            0x04,
+            0x05,
+            0x06,
+            0x80,
+            0x82,
+            0xFF,
+        ]
 
     def test_basic_commands(self):
         """Test device identification reading"""
-        self.assertEqual(str(self.ident), "DeviceIdentity")
-        self.assertEqual(str(self.control), "ModbusControl")
+        assert str(self.ident) == "DeviceIdentity"
+        assert str(self.control) == "ModbusControl"
 
     def test_modbus_device_identification_get(self):
         """Test device identification reading"""
-        self.assertEqual(self.ident[0x00], "Bashwork")
-        self.assertEqual(self.ident[0x01], "PTM")
-        self.assertEqual(self.ident[0x02], "1.0")
-        self.assertEqual(self.ident[0x03], "http://internets.com")
-        self.assertEqual(self.ident[0x04], "pymodbus")
-        self.assertEqual(self.ident[0x05], "bashwork")
-        self.assertEqual(self.ident[0x06], "unittest")
-        self.assertNotEqual(self.ident[0x07], "x")
-        self.assertNotEqual(self.ident[0x08], "x")
-        self.assertNotEqual(self.ident[0x10], "reserved")
-        self.assertEqual(self.ident[0x54], "")
+        assert self.ident[0x00] == "Bashwork"
+        assert self.ident[0x01] == "PTM"
+        assert self.ident[0x02] == "1.0"
+        assert self.ident[0x03] == "http://internets.com"
+        assert self.ident[0x04] == "pymodbus"
+        assert self.ident[0x05] == "bashwork"
+        assert self.ident[0x06] == "pytest"
+        assert self.ident[0x07] != "x"
+        assert self.ident[0x08] != "x"
+        assert self.ident[0x10] != "reserved"
+        assert not self.ident[0x54]
 
     def test_modbus_device_identification_summary(self):
         """Test device identification summary creation"""
         summary = sorted(self.ident.summary().values())
         expected = sorted(list(self.info.values())[:0x07])  # remove private
-        self.assertEqual(summary, expected)
+        assert summary == expected
 
     def test_modbus_device_identification_set(self):
         """Test a device identification writing"""
@@ -175,31 +184,31 @@ def test_modbus_device_identification_set(self):
         self.ident[0x10] = "public"
         self.ident[0x54] = "testing"
 
-        self.assertNotEqual("y", self.ident[0x07])
-        self.assertNotEqual("y", self.ident[0x08])
-        self.assertEqual("public", self.ident[0x10])
-        self.assertEqual("testing", self.ident[0x54])
+        assert self.ident[0x07] != "y"
+        assert self.ident[0x08] != "y"
+        assert self.ident[0x10] == "public"
+        assert self.ident[0x54] == "testing"
 
     def test_modbus_control_block_ascii_modes(self):
         """Test a server control block ascii mode"""
-        self.assertEqual(id(self.control), id(ModbusControlBlock()))
+        assert id(self.control) == id(ModbusControlBlock())
         self.control.Mode = "RTU"
-        self.assertEqual("RTU", self.control.Mode)
+        assert self.control.Mode == "RTU"
         self.control.Mode = "FAKE"
-        self.assertNotEqual("FAKE", self.control.Mode)
+        assert self.control.Mode != "FAKE"
 
     def test_modbus_control_block_counters(self):
         """Tests the MCB counters methods"""
-        self.assertEqual(0x0, self.control.Counter.BusMessage)
+        assert not self.control.Counter.BusMessage
         for _ in range(10):
             self.control.Counter.BusMessage += 1
             self.control.Counter.SlaveMessage += 1
-        self.assertEqual(10, self.control.Counter.BusMessage)
+        assert self.control.Counter.BusMessage == 10
         self.control.Counter.BusMessage = 0x00
-        self.assertEqual(0, self.control.Counter.BusMessage)
-        self.assertEqual(10, self.control.Counter.SlaveMessage)
+        assert not self.control.Counter.BusMessage
+        assert self.control.Counter.SlaveMessage == 10
         self.control.Counter.reset()
-        self.assertEqual(0, self.control.Counter.SlaveMessage)
+        assert not self.control.Counter.SlaveMessage
 
     def test_modbus_control_block_update(self):
         """Tests the MCB counters update methods"""
@@ -207,96 +216,96 @@ def test_modbus_control_block_update(self):
         self.control.Counter.BusMessage += 1
         self.control.Counter.SlaveMessage += 1
         self.control.Counter.update(values)
-        self.assertEqual(6, self.control.Counter.SlaveMessage)
-        self.assertEqual(6, self.control.Counter.BusMessage)
+        assert self.control.Counter.SlaveMessage == 6
+        assert self.control.Counter.BusMessage == 6
 
     def test_modbus_control_block_iterator(self):
         """Tests the MCB counters iterator"""
         self.control.Counter.reset()
         for _, count in self.control:
-            self.assertEqual(0, count)
+            assert not count
 
     def test_modbus_counters_handler_iterator(self):
         """Tests the MCB counters iterator"""
         self.control.Counter.reset()
         for _, count in self.control.Counter:
-            self.assertEqual(0, count)
+            assert not count
 
     def test_modbus_control_block_counter_summary(self):
         """Tests retrieving the current counter summary"""
-        self.assertEqual(0x00, self.control.Counter.summary())
+        assert not self.control.Counter.summary()
         for _ in range(10):
             self.control.Counter.BusMessage += 1
             self.control.Counter.SlaveMessage += 1
             self.control.Counter.SlaveNAK += 1
             self.control.Counter.BusCharacterOverrun += 1
-        self.assertEqual(0xA9, self.control.Counter.summary())
+        assert self.control.Counter.summary() == 0xA9
         self.control.Counter.reset()
-        self.assertEqual(0x00, self.control.Counter.summary())
+        assert not self.control.Counter.summary()
 
     def test_modbus_control_block_listen(self):
         """Test the MCB listen flag methods"""
         self.control.ListenOnly = False
-        self.assertEqual(self.control.ListenOnly, False)
+        assert not self.control.ListenOnly
         self.control.ListenOnly = not self.control.ListenOnly
-        self.assertEqual(self.control.ListenOnly, True)
+        assert self.control.ListenOnly
 
     def test_modbus_control_block_delimiter(self):
         """Tests the MCB delimiter setting methods"""
         self.control.Delimiter = b"\r"
-        self.assertEqual(self.control.Delimiter, b"\r")
+        assert self.control.Delimiter == b"\r"
         self.control.Delimiter = "="
-        self.assertEqual(self.control.Delimiter, b"=")
+        assert self.control.Delimiter == b"="
         self.control.Delimiter = 61
-        self.assertEqual(self.control.Delimiter, b"=")
+        assert self.control.Delimiter == b"="
 
     def test_modbus_control_block_diagnostic(self):
         """Tests the MCB delimiter setting methods"""
-        self.assertEqual([False] * 16, self.control.getDiagnosticRegister())
+        assert self.control.getDiagnosticRegister() == [False] * 16
         for i in (1, 3, 4, 6):
             self.control.setDiagnostic({i: True})
-        self.assertEqual(True, self.control.getDiagnostic(1))
-        self.assertEqual(False, self.control.getDiagnostic(2))
+        assert self.control.getDiagnostic(1)
+        assert not self.control.getDiagnostic(2)
         actual = [False, True, False, True, True, False, True] + [False] * 9
-        self.assertEqual(actual, self.control.getDiagnosticRegister())
+        assert actual == self.control.getDiagnosticRegister()
         for i in range(16):
             self.control.setDiagnostic({i: False})
 
     def test_modbus_control_block_invalid_diagnostic(self):
         """Tests querying invalid MCB counters methods"""
-        self.assertEqual(None, self.control.getDiagnostic(-1))
-        self.assertEqual(None, self.control.getDiagnostic(17))
-        self.assertEqual(None, self.control.getDiagnostic(None))
-        self.assertEqual(None, self.control.getDiagnostic([1, 2, 3]))
+        assert not self.control.getDiagnostic(-1)
+        assert not self.control.getDiagnostic(17)
+        assert not self.control.getDiagnostic(None)
+        assert not self.control.getDiagnostic([1, 2, 3])
 
     def test_clearing_control_events(self):
         """Test adding and clearing modbus events"""
-        self.assertEqual(self.control.Events, [])
+        assert self.control.Events == []
         event = ModbusEvent()
         self.control.addEvent(event)
-        self.assertEqual(self.control.Events, [event])
-        self.assertEqual(self.control.Counter.Event, 1)
+        assert self.control.Events == [event]
+        assert self.control.Counter.Event == 1
         self.control.clearEvents()
-        self.assertEqual(self.control.Events, [])
-        self.assertEqual(self.control.Counter.Event, 1)
+        assert self.control.Events == []
+        assert self.control.Counter.Event == 1
 
     def test_retrieving_control_events(self):
         """Test adding and removing a host"""
-        self.assertEqual(self.control.Events, [])
+        assert self.control.Events == []
         event = RemoteReceiveEvent()
         self.control.addEvent(event)
-        self.assertEqual(self.control.Events, [event])
+        assert self.control.Events == [event]
         packet = self.control.getEvents()
-        self.assertEqual(packet, b"\x40")
+        assert packet == b"\x40"
 
     def test_modbus_plus_statistics(self):
         """Test device identification reading"""
         default = [0x0000] * 55
         statistics = ModbusPlusStatistics()
-        self.assertEqual(default, statistics.encode())
+        assert default == statistics.encode()
         statistics.reset()
-        self.assertEqual(default, statistics.encode())
-        self.assertEqual(default, self.control.Plus.encode())
+        assert default == statistics.encode()
+        assert default == self.control.Plus.encode()
 
     def test_modbus_plus_statistics_helpers(self):
         """Test modbus plus statistics helper methods"""
@@ -351,5 +360,5 @@ def test_modbus_plus_statistics_helpers(self):
             [0, 0, 0, 0, 0, 0, 0, 0],
         ]
         stats_summary = list(statistics.summary())
-        self.assertEqual(sorted(summary), sorted(stats_summary))
-        self.assertEqual(0x00, sum(sum(value[1]) for value in statistics))
+        assert sorted(summary) == sorted(stats_summary)
+        assert not sum(sum(value[1]) for value in statistics)
diff --git a/test/test_diag_messages.py b/test/test_diag_messages.py
index 526bfe29f..dda7d7a87 100644
--- a/test/test_diag_messages.py
+++ b/test/test_diag_messages.py
@@ -1,5 +1,5 @@
 """Test diag messages."""
-import unittest
+import pytest
 
 from pymodbus.constants import ModbusPlusOperation
 from pymodbus.diag_message import (
@@ -45,103 +45,105 @@
 from pymodbus.exceptions import NotImplementedException
 
 
-class SimpleDataStoreTest(unittest.TestCase):
+class TestDataStore:
     """Unittest for the pymodbus.diag_message module."""
 
-    def setUp(self):
-        """Do setup."""
-        self.requests = [
-            (
-                RestartCommunicationsOptionRequest,
-                b"\x00\x01\x00\x00",
-                b"\x00\x01\xff\x00",
-            ),
-            (ReturnDiagnosticRegisterRequest, b"\x00\x02\x00\x00", b"\x00\x02\x00\x00"),
-            (
-                ChangeAsciiInputDelimiterRequest,
-                b"\x00\x03\x00\x00",
-                b"\x00\x03\x00\x00",
-            ),
-            (ForceListenOnlyModeRequest, b"\x00\x04\x00\x00", b"\x00\x04"),
-            (ReturnQueryDataRequest, b"\x00\x00\x00\x00", b"\x00\x00\x00\x00"),
-            (ClearCountersRequest, b"\x00\x0a\x00\x00", b"\x00\x0a\x00\x00"),
-            (ReturnBusMessageCountRequest, b"\x00\x0b\x00\x00", b"\x00\x0b\x00\x00"),
-            (
-                ReturnBusCommunicationErrorCountRequest,
-                b"\x00\x0c\x00\x00",
-                b"\x00\x0c\x00\x00",
-            ),
-            (
-                ReturnBusExceptionErrorCountRequest,
-                b"\x00\x0d\x00\x00",
-                b"\x00\x0d\x00\x00",
-            ),
-            (ReturnSlaveMessageCountRequest, b"\x00\x0e\x00\x00", b"\x00\x0e\x00\x00"),
-            (
-                ReturnSlaveNoResponseCountRequest,
-                b"\x00\x0f\x00\x00",
-                b"\x00\x0f\x00\x00",
-            ),
-            (ReturnSlaveNAKCountRequest, b"\x00\x10\x00\x00", b"\x00\x10\x00\x00"),
-            (ReturnSlaveBusyCountRequest, b"\x00\x11\x00\x00", b"\x00\x11\x00\x00"),
-            (
-                ReturnSlaveBusCharacterOverrunCountRequest,
-                b"\x00\x12\x00\x00",
-                b"\x00\x12\x00\x00",
-            ),
-            (ReturnIopOverrunCountRequest, b"\x00\x13\x00\x00", b"\x00\x13\x00\x00"),
-            (ClearOverrunCountRequest, b"\x00\x14\x00\x00", b"\x00\x14\x00\x00"),
-            (
-                GetClearModbusPlusRequest,
-                b"\x00\x15\x00\x00",
-                b"\x00\x15\x00\x00" + b"\x00\x00" * 55,
-            ),
-        ]
-
-        self.responses = [
-            # (DiagnosticStatusResponse,                     b"\x00\x00\x00\x00"),
-            # (DiagnosticStatusSimpleResponse,               b"\x00\x00\x00\x00"),
-            (ReturnQueryDataResponse, b"\x00\x00\x00\x00"),
-            (RestartCommunicationsOptionResponse, b"\x00\x01\x00\x00"),
-            (ReturnDiagnosticRegisterResponse, b"\x00\x02\x00\x00"),
-            (ChangeAsciiInputDelimiterResponse, b"\x00\x03\x00\x00"),
-            (ForceListenOnlyModeResponse, b"\x00\x04"),
-            (ReturnQueryDataResponse, b"\x00\x00\x00\x00"),
-            (ClearCountersResponse, b"\x00\x0a\x00\x00"),
-            (ReturnBusMessageCountResponse, b"\x00\x0b\x00\x00"),
-            (ReturnBusCommunicationErrorCountResponse, b"\x00\x0c\x00\x00"),
-            (ReturnBusExceptionErrorCountResponse, b"\x00\x0d\x00\x00"),
-            (ReturnSlaveMessageCountResponse, b"\x00\x0e\x00\x00"),
-            (ReturnSlaveNoResponseCountResponse, b"\x00\x0f\x00\x00"),
-            (ReturnSlaveNAKCountResponse, b"\x00\x10\x00\x00"),
-            (ReturnSlaveBusyCountResponse, b"\x00\x11\x00\x00"),
-            (ReturnSlaveBusCharacterOverrunCountResponse, b"\x00\x12\x00\x00"),
-            (ReturnIopOverrunCountResponse, b"\x00\x13\x00\x00"),
-            (ClearOverrunCountResponse, b"\x00\x14\x00\x00"),
-            (GetClearModbusPlusResponse, b"\x00\x15\x00\x04" + b"\x00\x00" * 55),
-        ]
-
-    def tearDown(self):
-        """Clean up the test environment"""
-        del self.requests
-        del self.responses
+    requests = [
+        (
+            RestartCommunicationsOptionRequest,
+            b"\x00\x01\x00\x00",
+            b"\x00\x01\xff\x00",
+        ),
+        (ReturnDiagnosticRegisterRequest, b"\x00\x02\x00\x00", b"\x00\x02\x00\x00"),
+        (
+            ChangeAsciiInputDelimiterRequest,
+            b"\x00\x03\x00\x00",
+            b"\x00\x03\x00\x00",
+        ),
+        (ForceListenOnlyModeRequest, b"\x00\x04\x00\x00", b"\x00\x04"),
+        (ReturnQueryDataRequest, b"\x00\x00\x00\x00", b"\x00\x00\x00\x00"),
+        (ClearCountersRequest, b"\x00\x0a\x00\x00", b"\x00\x0a\x00\x00"),
+        (ReturnBusMessageCountRequest, b"\x00\x0b\x00\x00", b"\x00\x0b\x00\x00"),
+        (
+            ReturnBusCommunicationErrorCountRequest,
+            b"\x00\x0c\x00\x00",
+            b"\x00\x0c\x00\x00",
+        ),
+        (
+            ReturnBusExceptionErrorCountRequest,
+            b"\x00\x0d\x00\x00",
+            b"\x00\x0d\x00\x00",
+        ),
+        (ReturnSlaveMessageCountRequest, b"\x00\x0e\x00\x00", b"\x00\x0e\x00\x00"),
+        (
+            ReturnSlaveNoResponseCountRequest,
+            b"\x00\x0f\x00\x00",
+            b"\x00\x0f\x00\x00",
+        ),
+        (ReturnSlaveNAKCountRequest, b"\x00\x10\x00\x00", b"\x00\x10\x00\x00"),
+        (ReturnSlaveBusyCountRequest, b"\x00\x11\x00\x00", b"\x00\x11\x00\x00"),
+        (
+            ReturnSlaveBusCharacterOverrunCountRequest,
+            b"\x00\x12\x00\x00",
+            b"\x00\x12\x00\x00",
+        ),
+        (ReturnIopOverrunCountRequest, b"\x00\x13\x00\x00", b"\x00\x13\x00\x00"),
+        (ClearOverrunCountRequest, b"\x00\x14\x00\x00", b"\x00\x14\x00\x00"),
+        (
+            GetClearModbusPlusRequest,
+            b"\x00\x15\x00\x00",
+            b"\x00\x15\x00\x00" + b"\x00\x00" * 55,
+        ),
+    ]
+
+    responses = [
+        # (DiagnosticStatusResponse,                     b"\x00\x00\x00\x00"),
+        # (DiagnosticStatusSimpleResponse,               b"\x00\x00\x00\x00"),
+        (ReturnQueryDataResponse, b"\x00\x00\x00\x00"),
+        (RestartCommunicationsOptionResponse, b"\x00\x01\x00\x00"),
+        (ReturnDiagnosticRegisterResponse, b"\x00\x02\x00\x00"),
+        (ChangeAsciiInputDelimiterResponse, b"\x00\x03\x00\x00"),
+        (ForceListenOnlyModeResponse, b"\x00\x04"),
+        (ReturnQueryDataResponse, b"\x00\x00\x00\x00"),
+        (ClearCountersResponse, b"\x00\x0a\x00\x00"),
+        (ReturnBusMessageCountResponse, b"\x00\x0b\x00\x00"),
+        (ReturnBusCommunicationErrorCountResponse, b"\x00\x0c\x00\x00"),
+        (ReturnBusExceptionErrorCountResponse, b"\x00\x0d\x00\x00"),
+        (ReturnSlaveMessageCountResponse, b"\x00\x0e\x00\x00"),
+        (ReturnSlaveNoResponseCountResponse, b"\x00\x0f\x00\x00"),
+        (ReturnSlaveNAKCountResponse, b"\x00\x10\x00\x00"),
+        (ReturnSlaveBusyCountResponse, b"\x00\x11\x00\x00"),
+        (ReturnSlaveBusCharacterOverrunCountResponse, b"\x00\x12\x00\x00"),
+        (ReturnIopOverrunCountResponse, b"\x00\x13\x00\x00"),
+        (ClearOverrunCountResponse, b"\x00\x14\x00\x00"),
+        (GetClearModbusPlusResponse, b"\x00\x15\x00\x04" + b"\x00\x00" * 55),
+    ]
+
+    def test_diagnostic_encode_decode(self):
+        """Testing diagnostic request/response can be decoded and encoded."""
+        for msg in (DiagnosticStatusRequest, DiagnosticStatusResponse):
+            msg_obj = msg()
+            data = b"\x00\x01\x02\x03"
+            msg_obj.decode(data)
+            result = msg_obj.encode()
+            assert data == result
 
     def test_diagnostic_requests_decode(self):
         """Testing diagnostic request messages encoding"""
         for msg, enc, _ in self.requests:
             handle = DiagnosticStatusRequest()
             handle.decode(enc)
-            self.assertEqual(handle.sub_function_code, msg.sub_function_code)
+            assert handle.sub_function_code == msg.sub_function_code
+            encoded = handle.encode()
+            assert enc == encoded
 
     def test_diagnostic_simple_requests(self):
         """Testing diagnostic request messages encoding"""
         request = DiagnosticStatusSimpleRequest(b"\x12\x34")
         request.sub_function_code = 0x1234
-        self.assertRaises(
-            NotImplementedException,
-            lambda: request.execute(),  # pylint: disable=unnecessary-lambda
-        )
-        self.assertEqual(request.encode(), b"\x12\x34\x12\x34")
+        with pytest.raises(NotImplementedException):
+            request.execute()
+        assert request.encode() == b"\x12\x34\x12\x34"
         DiagnosticStatusSimpleResponse(None)
 
     def test_diagnostic_response_decode(self):
@@ -149,52 +151,52 @@ def test_diagnostic_response_decode(self):
         for msg, enc, _ in self.requests:
             handle = DiagnosticStatusResponse()
             handle.decode(enc)
-            self.assertEqual(handle.sub_function_code, msg.sub_function_code)
+            assert handle.sub_function_code == msg.sub_function_code
 
     def test_diagnostic_requests_encode(self):
         """Testing diagnostic request messages encoding"""
         for msg, enc, _ in self.requests:
-            self.assertEqual(msg().encode(), enc)
+            assert msg().encode() == enc
 
     def test_diagnostic_execute(self):
         """Testing diagnostic message execution"""
         for message, encoded, executed in self.requests:
             encoded = message().execute().encode()
-            self.assertEqual(encoded, executed)
+            assert encoded == executed
 
     def test_return_query_data_request(self):
         """Testing diagnostic message execution"""
         message = ReturnQueryDataRequest([0x0000] * 2)
-        self.assertEqual(message.encode(), b"\x00\x00\x00\x00\x00\x00")
+        assert message.encode() == b"\x00\x00\x00\x00\x00\x00"
         message = ReturnQueryDataRequest(0x0000)
-        self.assertEqual(message.encode(), b"\x00\x00\x00\x00")
+        assert message.encode() == b"\x00\x00\x00\x00"
 
     def test_return_query_data_response(self):
         """Testing diagnostic message execution"""
         message = ReturnQueryDataResponse([0x0000] * 2)
-        self.assertEqual(message.encode(), b"\x00\x00\x00\x00\x00\x00")
+        assert message.encode() == b"\x00\x00\x00\x00\x00\x00"
         message = ReturnQueryDataResponse(0x0000)
-        self.assertEqual(message.encode(), b"\x00\x00\x00\x00")
+        assert message.encode() == b"\x00\x00\x00\x00"
 
     def test_restart_cmmunications_option(self):
         """Testing diagnostic message execution"""
         request = RestartCommunicationsOptionRequest(True)
-        self.assertEqual(request.encode(), b"\x00\x01\xff\x00")
+        assert request.encode() == b"\x00\x01\xff\x00"
         request = RestartCommunicationsOptionRequest(False)
-        self.assertEqual(request.encode(), b"\x00\x01\x00\x00")
+        assert request.encode() == b"\x00\x01\x00\x00"
 
         response = RestartCommunicationsOptionResponse(True)
-        self.assertEqual(response.encode(), b"\x00\x01\xff\x00")
+        assert response.encode() == b"\x00\x01\xff\x00"
         response = RestartCommunicationsOptionResponse(False)
-        self.assertEqual(response.encode(), b"\x00\x01\x00\x00")
+        assert response.encode() == b"\x00\x01\x00\x00"
 
     def test_get_clear_modbus_plus_request_execute(self):
         """Testing diagnostic message execution"""
         request = GetClearModbusPlusRequest(data=ModbusPlusOperation.ClearStatistics)
         response = request.execute()
-        self.assertEqual(response.message, ModbusPlusOperation.ClearStatistics)
+        assert response.message == ModbusPlusOperation.ClearStatistics
 
         request = GetClearModbusPlusRequest(data=ModbusPlusOperation.GetStatistics)
         response = request.execute()
         resp = [ModbusPlusOperation.GetStatistics]
-        self.assertEqual(response.message, resp + [0x00] * 55)
+        assert response.message == resp + [0x00] * 55
diff --git a/test/test_events.py b/test/test_events.py
index 7eac3de03..bfe68bda9 100644
--- a/test/test_events.py
+++ b/test/test_events.py
@@ -1,5 +1,5 @@
 """Test events."""
-import unittest
+import pytest
 
 from pymodbus.events import (
     CommunicationRestartEvent,
@@ -11,41 +11,37 @@
 from pymodbus.exceptions import NotImplementedException, ParameterException
 
 
-class ModbusEventsTest(unittest.TestCase):
+class TestEvents:
     """Unittest for the pymodbus.device module."""
 
-    def setUp(self):
-        """Set up the test environment"""
-
-    def tearDown(self):
-        """Clean up the test environment"""
-
     def test_modbus_event_base_class(self):
         """Test modbus event base class."""
         event = ModbusEvent()
-        self.assertRaises(NotImplementedException, event.encode)
-        self.assertRaises(NotImplementedException, lambda: event.decode(None))
+        with pytest.raises(NotImplementedException):
+            event.encode()
+        with pytest.raises(NotImplementedException):
+            event.decode(None)
 
     def test_remote_receive_event(self):
         """Test remove receive event."""
         event = RemoteReceiveEvent()
         event.decode(b"\x70")
-        self.assertTrue(event.overrun)
-        self.assertTrue(event.listen)
-        self.assertTrue(event.broadcast)
+        assert event.overrun
+        assert event.listen
+        assert event.broadcast
 
     def test_remote_sent_event(self):
         """Test remote sent event."""
         event = RemoteSendEvent()
         result = event.encode()
-        self.assertEqual(result, b"\x40")
+        assert result == b"\x40"
         event.decode(b"\x7f")
-        self.assertTrue(event.read)
-        self.assertTrue(event.slave_abort)
-        self.assertTrue(event.slave_busy)
-        self.assertTrue(event.slave_nak)
-        self.assertTrue(event.write_timeout)
-        self.assertTrue(event.listen)
+        assert event.read
+        assert event.slave_abort
+        assert event.slave_busy
+        assert event.slave_nak
+        assert event.write_timeout
+        assert event.listen
 
     def test_remote_sent_event_encode(self):
         """Test remote sent event encode."""
@@ -59,22 +55,24 @@ def test_remote_sent_event_encode(self):
         }
         event = RemoteSendEvent(**arguments)
         result = event.encode()
-        self.assertEqual(result, b"\x7f")
+        assert result == b"\x7f"
 
     def test_entered_listen_mode_event(self):
         """Test entered listen mode event."""
         event = EnteredListenModeEvent()
         result = event.encode()
-        self.assertEqual(result, b"\x04")
+        assert result == b"\x04"
         event.decode(b"\x04")
-        self.assertEqual(event.value, 0x04)
-        self.assertRaises(ParameterException, lambda: event.decode(b"\x00"))
+        assert event.value == 0x04
+        with pytest.raises(ParameterException):
+            event.decode(b"\x00")
 
     def test_communication_restart_event(self):
         """Test communication restart event."""
         event = CommunicationRestartEvent()
         result = event.encode()
-        self.assertEqual(result, b"\x00")
+        assert result == b"\x00"
         event.decode(b"\x00")
-        self.assertEqual(event.value, 0x00)
-        self.assertRaises(ParameterException, lambda: event.decode(b"\x04"))
+        assert not event.value
+        with pytest.raises(ParameterException):
+            event.decode(b"\x04")
diff --git a/test/test_example_client_server.py b/test/test_example_client_server.py
new file mode 100755
index 000000000..e80cd9436
--- /dev/null
+++ b/test/test_example_client_server.py
@@ -0,0 +1,133 @@
+"""Test example server/client sync/async
+
+This is a thorough test of the generic examples
+(in principle examples that are used in other
+examples, like run a server).
+"""
+import asyncio
+import logging
+from threading import Thread
+from time import sleep
+
+import pytest
+import pytest_asyncio
+
+from examples.client_async import run_async_client, setup_async_client
+from examples.client_calls import run_sync_calls
+from examples.client_sync import run_sync_client, setup_sync_client
+from examples.client_test import run_async_calls as run_async_simple_calls
+from examples.helper import get_commandline
+from examples.server_async import run_async_server, setup_server
+from examples.server_sync import run_sync_server
+from pymodbus import pymodbus_apply_logging_config
+from pymodbus.server import ServerAsyncStop, ServerStop
+
+
+_logger = logging.getLogger()
+_logger.setLevel("DEBUG")
+pymodbus_apply_logging_config("DEBUG")
+TEST_COMMS_FRAMER = [
+    ("tcp", "socket", 5020),
+    ("tcp", "rtu", 5020),
+    ("tls", "tls", 5020),
+    ("udp", "socket", 5020),
+    ("udp", "rtu", 5020),
+    ("serial", "rtu", "socket://127.0.0.1:5020"),
+    # awaiting fix: ("serial", "ascii", "socket://127.0.0.1:5020"),
+    # awaiting fix: ("serial", "binary", "socket://127.0.0.1:5020"),
+]
+
+
+@pytest_asyncio.fixture(name="mock_run_server")
+async def _helper_server(
+    test_comm,
+    test_framer,
+    test_port,
+):
+    """Run server."""
+    cmdline = [
+        "--comm",
+        test_comm,
+        "--port",
+        str(test_port),
+        "--framer",
+        test_framer,
+        "--baudrate",
+        "9600",
+        "--log",
+        "debug",
+    ]
+    run_args = setup_server(cmdline=cmdline)
+    task = asyncio.create_task(run_async_server(run_args))
+    await asyncio.sleep(0.1)
+    yield
+    await ServerAsyncStop()
+    task.cancel()
+    await task
+
+
+def test_get_commandline():
+    """Test helper get_commandline()"""
+    args = get_commandline(cmdline=["--log", "info"])
+    assert args.log == "info"
+    assert args.host == "127.0.0.1"
+
+
+@pytest.mark.xdist_group(name="server_serialize")
+@pytest.mark.parametrize(("test_comm", "test_framer", "test_port"), TEST_COMMS_FRAMER)
+async def test_exp_async_server_client(
+    test_comm,
+    test_framer,
+    test_port,
+    mock_run_server,
+):
+    """Run async client and server."""
+    assert not mock_run_server
+    cmdline = [
+        "--comm",
+        test_comm,
+        "--host",
+        "127.0.0.1",
+        "--framer",
+        test_framer,
+        "--port",
+        str(test_port),
+        "--baudrate",
+        "9600",
+        "--log",
+        "debug",
+    ]
+    test_client = setup_async_client(cmdline=cmdline)
+    await run_async_client(test_client, modbus_calls=run_async_simple_calls)
+
+
+@pytest.mark.xdist_group(name="server_serialize")
+@pytest.mark.parametrize(
+    ("test_comm", "test_framer", "test_port"), [TEST_COMMS_FRAMER[0]]
+)
+def test_exp_sync_server_client(
+    test_comm,
+    test_framer,
+    test_port,
+):
+    """Run sync client and server."""
+    cmdline = [
+        "--comm",
+        test_comm,
+        "--port",
+        str(test_port),
+        "--baudrate",
+        "9600",
+        "--log",
+        "debug",
+        "--framer",
+        test_framer,
+    ]
+    run_args = setup_server(cmdline=cmdline)
+    thread = Thread(target=run_sync_server, args=(run_args,))
+    thread.daemon = True
+    thread.start()
+    sleep(1)
+    test_client = setup_sync_client(cmdline=cmdline)
+    run_sync_client(test_client, modbus_calls=run_sync_calls)
+    ServerStop()
diff --git a/test/test_examples.py b/test/test_examples.py
index 1fda68526..7803510f8 100755
--- a/test/test_examples.py
+++ b/test/test_examples.py
@@ -1,157 +1,182 @@
-"""Test client async."""
+"""Test examples to ensure they run
+
+the following are excluded:
+    client_async.py
+    client_calls.py
+    client_sync.py
+    helper.py
+    server_async.py
+    server_sync.py
+
+they represent generic examples and
+are tested in
+    test_example_client_server.py
+a lot more thoroughly.
+"""
 import asyncio
 import logging
-from threading import Thread
-from time import sleep
 
 import pytest
 import pytest_asyncio
 
+from examples.build_bcd_payload import BcdPayloadBuilder, BcdPayloadDecoder
 from examples.client_async import run_async_client, setup_async_client
-from examples.client_calls import run_async_calls, run_sync_calls
+from examples.client_custom_msg import run_custom_client
 from examples.client_payload import run_payload_calls
-from examples.client_sync import run_sync_client, setup_sync_client
-from examples.helper import Commandline
-
-# from examples.modbus_forwarder import run_forwarder
+from examples.client_test import run_async_calls as run_client_test
+from examples.message_generator import generate_messages
+from examples.message_parser import parse_messages
 from examples.server_async import run_async_server, setup_server
+from examples.server_callback import run_callback_server
 from examples.server_payload import setup_payload_server
-from examples.server_sync import run_sync_server
+from examples.server_simulator import run_server_simulator, setup_simulator
+from examples.server_updating import run_updating_server, setup_updating_server
 from pymodbus import pymodbus_apply_logging_config
-from pymodbus.server import ServerAsyncStop, ServerStop
-from pymodbus.transaction import (
-    ModbusAsciiFramer,
-    ModbusBinaryFramer,
-    ModbusRtuFramer,
-    ModbusSocketFramer,
-    ModbusTlsFramer,
-)
+from pymodbus.server import ServerAsyncStop
+
+
+# from examples.serial_forwarder import run_forwarder
 
 
 _logger = logging.getLogger()
 _logger.setLevel("DEBUG")
-TEST_COMMS_FRAMER = [
-    ("tcp", ModbusSocketFramer, 5020),
-    ("tcp", ModbusRtuFramer, 5021),
-    ("tls", ModbusTlsFramer, 5020),
-    ("udp", ModbusSocketFramer, 5020),
-    ("udp", ModbusRtuFramer, 5021),
-    ("serial", ModbusRtuFramer, 5020),
-    ("serial", ModbusAsciiFramer, 5021),
-    ("serial", ModbusBinaryFramer, 5022),
+pymodbus_apply_logging_config("DEBUG")
+
+
+CMDARGS = [
+    "--comm",
+    "tcp",
+    "--port",
+    "5020",
+    "--baudrate",
+    "9600",
+    "--log",
+    "debug",
+    "--framer",
+    "socket",
 ]
 
 
 @pytest_asyncio.fixture(name="mock_run_server")
-async def _helper_server(
-    test_comm,
-    test_framer,
-    test_port_offset,
-    test_port,
-):
+async def _helper_server():
     """Run server."""
-    if pytest.IS_WINDOWS and test_comm == "serial":
-        yield
-        return
-    args = Commandline.copy()
-    args.comm = test_comm
-    args.framer = test_framer
-    args.port = test_port + test_port_offset
-    if test_comm == "serial":
-        args.port = f"socket://127.0.0.1:{args.port}"
-    run_args = setup_server(args)
-    asyncio.create_task(run_async_server(run_args))
+    run_args = setup_server(cmdline=CMDARGS)
+    task = asyncio.create_task(run_async_server(run_args))
     await asyncio.sleep(0.1)
     yield
     await ServerAsyncStop()
+    await asyncio.sleep(0.1)
+    task.cancel()
+    await task
+    await asyncio.sleep(0.1)
 
 
-async def run_client(test_comm, test_type, args=Commandline.copy()):
-    """Help run async client."""
-
-    args.comm = test_comm
-    if test_comm == "serial":
-        args.port = f"socket://127.0.0.1:{args.port}"
-    test_client = setup_async_client(args=args)
-    if not test_type:
-        await run_async_client(test_client)
-    else:
-        await run_async_client(test_client, modbus_calls=test_type)
+@pytest.mark.xdist_group(name="server_serialize")
+async def test_exp_server_client_payload():
+    """Test server/client with payload."""
+    run_args = setup_payload_server(cmdline=CMDARGS)
+    task = asyncio.create_task(run_async_server(run_args))
     await asyncio.sleep(0.1)
+    testclient = setup_async_client(cmdline=CMDARGS)
+    await run_async_client(testclient, modbus_calls=run_payload_calls)
+    await asyncio.sleep(0.1)
+    await ServerAsyncStop()
+    await asyncio.sleep(0.1)
+    task.cancel()
+    await task
 
 
-@pytest.mark.parametrize("test_port_offset", [10])
-@pytest.mark.parametrize("test_comm, test_framer, test_port", TEST_COMMS_FRAMER)
-async def test_exp_async_server_client(
-    test_comm,
-    test_framer,
-    test_port_offset,
-    test_port,
-    mock_run_server,
-):
-    """Run async client and server."""
-    # JAN WAITING
-    if pytest.IS_WINDOWS and test_comm == "serial":
-        return
-    if test_comm in {"tcp", "tls"}:
-        return
+@pytest.mark.xdist_group(name="server_serialize")
+async def test_exp_client_test(mock_run_server):
+    """Test client used for fast testing."""
     assert not mock_run_server
-    args = Commandline.copy()
-    args.framer = test_framer
-    args.comm = test_comm
-    args.port = test_port + test_port_offset
-    await run_client(test_comm, None, args=args)
 
+    testclient = setup_async_client(cmdline=CMDARGS)
+    await run_async_client(testclient, modbus_calls=run_client_test)
 
-@pytest.mark.parametrize("test_port_offset", [20])
-@pytest.mark.parametrize("test_comm, test_framer, test_port", [TEST_COMMS_FRAMER[0]])
-def test_exp_sync_server_client(
-    test_comm,
-    test_framer,
-    test_port_offset,
-    test_port,
-):
-    """Run sync client and server."""
-    args = Commandline.copy()
-    args.comm = test_comm
-    args.port = test_port + test_port_offset
-    args.framer = test_framer
-    run_args = setup_server(args)
-    thread = Thread(target=run_sync_server, args=(run_args,))
-    thread.daemon = True
-    thread.start()
-    sleep(1)
-    test_client = setup_sync_client(args=args)
-    run_sync_client(test_client, modbus_calls=run_sync_calls)
-    ServerStop()
-
-
-# JAN
-@pytest.mark.parametrize("test_port_offset", [30])
-@pytest.mark.parametrize("test_comm, test_framer, test_port", TEST_COMMS_FRAMER)
-async def xtest_exp_client_calls(
-    test_comm,
-    test_framer,
-    test_port_offset,
-    test_port,
-    mock_run_server,
-):
-    """Test client-server async with different framers and calls."""
+
+@pytest.mark.parametrize("framer", ["socket", "rtu"])
+async def test_exp_message_generator(framer):
+    """Test all message generator."""
+    generate_messages(cmdline=["--framer", framer])
+
+
+@pytest.mark.xdist_group(name="server_serialize")
+async def test_exp_server_simulator():
+    """Test server simulator."""
+    cmdargs = ["--log", "debug", "--port", "5020"]
+    run_args = setup_simulator(cmdline=cmdargs)
+    task = asyncio.create_task(run_server_simulator(run_args))
+    await asyncio.sleep(0.1)
+    testclient = setup_async_client(cmdline=CMDARGS)
+    await run_async_client(testclient, modbus_calls=run_client_test)
+    await asyncio.sleep(0.1)
+    await ServerAsyncStop()
+    await asyncio.sleep(0.1)
+    task.cancel()
+    await task
+
+
+@pytest.mark.xdist_group(name="server_serialize")
+async def test_exp_updating_server():
+    """Test server simulator."""
+    run_args = setup_updating_server(cmdline=CMDARGS)
+    task = asyncio.create_task(run_updating_server(run_args))
+    await asyncio.sleep(0.1)
+    testclient = setup_async_client(cmdline=CMDARGS)
+    await run_async_client(testclient, modbus_calls=run_client_test)
+    await asyncio.sleep(0.1)
+    await ServerAsyncStop()
+    await asyncio.sleep(0.1)
+    task.cancel()
+    await task
+
+
+def test_exp_build_bcd_payload():
+    """Test build bcd payload."""
+    builder = BcdPayloadBuilder()
+    decoder = BcdPayloadDecoder(builder)
+    assert str(decoder)
+
+
+def test_exp_message_parser():
+    """Test message parser."""
+    parse_messages(["--framer", "socket", "-m", "000100000006010100200001"])
+    parse_messages(["--framer", "socket", "-m", "00010000000401010101"])
+
+
+@pytest.mark.xdist_group(name="server_serialize")
+async def test_exp_server_callback():
+    """Test server/client with payload."""
+    task = asyncio.create_task(run_callback_server(cmdline=CMDARGS))
+    await asyncio.sleep(0.1)
+    testclient = setup_async_client(cmdline=CMDARGS)
+    await run_async_client(testclient, modbus_calls=run_client_test)
+    await asyncio.sleep(0.1)
+    await ServerAsyncStop()
+    await asyncio.sleep(0.1)
+    task.cancel()
+    await task
+
+
+@pytest.mark.xdist_group(name="server_serialize")
+async def test_exp_client_custom_msg(mock_run_server):
+    """Test client with custom message."""
     assert not mock_run_server
-    if test_comm == "serial" and test_framer in (ModbusAsciiFramer, ModbusBinaryFramer):
-        return
-    if pytest.IS_WINDOWS and test_comm == "serial":
-        return
-    args = Commandline.copy()
-    args.framer = test_framer
-    args.comm = test_comm
-    args.port = test_port + test_port_offset
-    await run_client(test_comm, run_async_calls, args=args)
+
+    run_custom_client()
 
 
-@pytest.mark.parametrize("test_port_offset", [40])
-@pytest.mark.parametrize("test_comm, test_framer, test_port", [TEST_COMMS_FRAMER[0]])
-async def test_exp_forwarder(
+# to be updated:
+#   modbus_forwarder.py
+#
+# to be converted:
+#   v2.5.3
+
+
+# @pytest.mark.parametrize("test_port_offset", [40])
+# @pytest.mark.parametrize("test_comm, test_framer, test_port", [TEST_COMMS_FRAMER[0]])
+async def xtest_exp_forwarder(
     test_comm,
     test_framer,
     test_port_offset,
@@ -163,8 +188,6 @@ async def test_exp_forwarder(
     if pytest.IS_WINDOWS:
         return
     print(test_comm, test_framer, test_port_offset, test_port)
-    pymodbus_apply_logging_config()
-    # cmd_args = Commandline.copy()
     # cmd_args.comm = test_comm
     # cmd_args.framer = test_framer
     # cmd_args.port = test_port + test_port_offset + 1
@@ -206,31 +229,3 @@ async def test_exp_forwarder(
     # await ServerAsyncStop()
     # await asyncio.sleep(0.1)
     # task.cancel()
-
-
-@pytest.mark.parametrize("test_port_offset", [50])
-@pytest.mark.parametrize("test_comm, test_framer, test_port", [TEST_COMMS_FRAMER[0]])
-async def test_exp_payload(
-    test_comm,
-    test_framer,
-    test_port_offset,
-    test_port,
-):
-    """Test server/client with payload."""
-    pymodbus_apply_logging_config()
-    args = Commandline.copy()
-    args.port = test_port + test_port_offset
-    args.comm = test_comm
-    args.framer = test_framer
-    run_args = setup_payload_server(args)
-    task = asyncio.create_task(run_async_server(run_args))
-    await asyncio.sleep(0.1)
-    testclient = setup_async_client(args)
-    await run_async_client(testclient, modbus_calls=run_payload_calls)
-    await asyncio.sleep(0.1)
-    await ServerAsyncStop()
-    try:
-        await asyncio.sleep(0.1)
-    except asyncio.CancelledError:
-        pass
-    task.cancel()
diff --git a/test/test_exceptions.py b/test/test_exceptions.py
index fe9f8e327..369ad40fd 100644
--- a/test/test_exceptions.py
+++ b/test/test_exceptions.py
@@ -1,5 +1,5 @@
 """Test exceptions."""
-import unittest
+import pytest
 
 from pymodbus.exceptions import (
     ConnectionException,
@@ -10,28 +10,19 @@
 )
 
 
-class SimpleExceptionsTest(unittest.TestCase):
+class TestExceptions:  # pylint: disable=too-few-public-methods
     """Unittest for the pymodbus.exceptions module."""
 
-    def setUp(self):
-        """Initialize the test environment"""
-        self.exceptions = [
-            ModbusException("bad base"),
-            ModbusIOException("bad register"),
-            ParameterException("bad parameter"),
-            NotImplementedException("bad function"),
-            ConnectionException("bad connection"),
-        ]
-
-    def tearDown(self):
-        """Clean up the test environment"""
+    exceptions = [
+        ModbusException("bad base"),
+        ModbusIOException("bad register"),
+        ParameterException("bad parameter"),
+        NotImplementedException("bad function"),
+        ConnectionException("bad connection"),
+    ]
 
     def test_exceptions(self):
         """Test all module exceptions"""
         for exc in self.exceptions:
-            try:
+            with pytest.raises(ModbusException, match="Modbus Error:"):
                 raise exc
-            except ModbusException as exc:
-                self.assertTrue("Modbus Error:" in str(exc))
-                return
-            self.fail("Excepted a ModbusExceptions")
diff --git a/test/test_factory.py b/test/test_factory.py
index dbddaae0d..ea2d4902c 100644
--- a/test/test_factory.py
+++ b/test/test_factory.py
@@ -1,5 +1,5 @@
 """Test factory."""
-import unittest
+import pytest
 
 from pymodbus.exceptions import MessageRegisterException, ModbusException
 from pymodbus.factory import ClientDecoder, ServerDecoder
@@ -11,154 +11,143 @@ def _raise_exception(_):
     raise ModbusException("something")
 
 
-class SimpleFactoryTest(unittest.TestCase):
+class TestFactory:
     """Unittest for the pymod.exceptions module."""
 
-    def setUp(self):
-        """Initialize the test environment"""
+    client = None
+    server = None
+    request = (
+        (0x01, b"\x01\x00\x01\x00\x01"),  # read coils
+        (0x02, b"\x02\x00\x01\x00\x01"),  # read discrete inputs
+        (0x03, b"\x03\x00\x01\x00\x01"),  # read holding registers
+        (0x04, b"\x04\x00\x01\x00\x01"),  # read input registers
+        (0x05, b"\x05\x00\x01\x00\x01"),  # write single coil
+        (0x06, b"\x06\x00\x01\x00\x01"),  # write single register
+        (0x07, b"\x07"),  # read exception status
+        (0x08, b"\x08\x00\x00\x00\x00"),  # read diagnostic
+        (0x0B, b"\x0b"),  # get comm event counters
+        (0x0C, b"\x0c"),  # get comm event log
+        (0x0F, b"\x0f\x00\x01\x00\x08\x01\x00\xff"),  # write multiple coils
+        (0x10, b"\x10\x00\x01\x00\x02\x04\0xff\xff"),  # write multiple registers
+        (0x11, b"\x11"),  # report slave id
+        (
+            0x14,
+            b"\x14\x0e\x06\x00\x04\x00\x01\x00\x02\x06\x00\x03\x00\x09\x00\x02",
+        ),  # read file record
+        (
+            0x15,
+            b"\x15\x0d\x06\x00\x04\x00\x07\x00\x03\x06\xaf\x04\xbe\x10\x0d",
+        ),  # write file record
+        (0x16, b"\x16\x00\x01\x00\xff\xff\x00"),  # mask write register
+        (
+            0x17,
+            b"\x17\x00\x01\x00\x01\x00\x01\x00\x01\x02\x12\x34",
+        ),  # r/w multiple regs
+        (0x18, b"\x18\x00\x01"),  # read fifo queue
+        (0x2B, b"\x2b\x0e\x01\x00"),  # read device identification
+    )
+
+    response = (
+        (0x01, b"\x01\x01\x01"),  # read coils
+        (0x02, b"\x02\x01\x01"),  # read discrete inputs
+        (0x03, b"\x03\x02\x01\x01"),  # read holding registers
+        (0x04, b"\x04\x02\x01\x01"),  # read input registers
+        (0x05, b"\x05\x00\x01\x00\x01"),  # write single coil
+        (0x06, b"\x06\x00\x01\x00\x01"),  # write single register
+        (0x07, b"\x07\x00"),  # read exception status
+        (0x08, b"\x08\x00\x00\x00\x00"),  # read diagnostic
+        (0x0B, b"\x0b\x00\x00\x00\x00"),  # get comm event counters
+        (0x0C, b"\x0c\x08\x00\x00\x01\x08\x01\x21\x20\x00"),  # get comm event log
+        (0x0F, b"\x0f\x00\x01\x00\x08"),  # write multiple coils
+        (0x10, b"\x10\x00\x01\x00\x02"),  # write multiple registers
+        (0x11, b"\x11\x03\x05\x01\x54"),  # report slave id (device specific)
+        (
+            0x14,
+            b"\x14\x0c\x05\x06\x0d\xfe\x00\x20\x05\x06\x33\xcd\x00\x40",
+        ),  # read file record
+        (
+            0x15,
+            b"\x15\x0d\x06\x00\x04\x00\x07\x00\x03\x06\xaf\x04\xbe\x10\x0d",
+        ),  # write file record
+        (0x16, b"\x16\x00\x01\x00\xff\xff\x00"),  # mask write register
+        (0x17, b"\x17\x02\x12\x34"),  # read/write multiple registers
+        (0x18, b"\x18\x00\x01\x00\x01\x00\x00"),  # read fifo queue
+        (
+            0x2B,
+            b"\x2b\x0e\x01\x01\x00\x00\x01\x00\x01\x77",
+        ),  # read device identification
+    )
+
+    exception = (
+        (0x81, b"\x81\x01\xd0\x50"),  # illegal function exception
+        (0x82, b"\x82\x02\x90\xa1"),  # illegal data address exception
+        (0x83, b"\x83\x03\x50\xf1"),  # illegal data value exception
+        (0x84, b"\x84\x04\x13\x03"),  # skave device failure exception
+        (0x85, b"\x85\x05\xd3\x53"),  # acknowledge exception
+        (0x86, b"\x86\x06\x93\xa2"),  # slave device busy exception
+        (0x87, b"\x87\x08\x53\xf2"),  # memory parity exception
+        (0x88, b"\x88\x0a\x16\x06"),  # gateway path unavailable exception
+        (0x89, b"\x89\x0b\xd6\x56"),  # gateway target failed exception
+    )
+
+    bad = (
+        (0x80, b"\x80\x00\x00\x00"),  # Unknown Function
+        (0x81, b"\x81\x00\x00\x00"),  # error message
+    )
+
+    @pytest.fixture(autouse=True)
+    def _setup(self):
+        """Do common setup function."""
         self.client = ClientDecoder()
         self.server = ServerDecoder()
-        self.request = (
-            (0x01, b"\x01\x00\x01\x00\x01"),  # read coils
-            (0x02, b"\x02\x00\x01\x00\x01"),  # read discrete inputs
-            (0x03, b"\x03\x00\x01\x00\x01"),  # read holding registers
-            (0x04, b"\x04\x00\x01\x00\x01"),  # read input registers
-            (0x05, b"\x05\x00\x01\x00\x01"),  # write single coil
-            (0x06, b"\x06\x00\x01\x00\x01"),  # write single register
-            (0x07, b"\x07"),  # read exception status
-            (0x08, b"\x08\x00\x00\x00\x00"),  # read diagnostic
-            (0x0B, b"\x0b"),  # get comm event counters
-            (0x0C, b"\x0c"),  # get comm event log
-            (0x0F, b"\x0f\x00\x01\x00\x08\x01\x00\xff"),  # write multiple coils
-            (0x10, b"\x10\x00\x01\x00\x02\x04\0xff\xff"),  # write multiple registers
-            (0x11, b"\x11"),  # report slave id
-            (
-                0x14,
-                b"\x14\x0e\x06\x00\x04\x00\x01\x00\x02\x06\x00\x03\x00\x09\x00\x02",
-            ),  # read file record
-            (
-                0x15,
-                b"\x15\x0d\x06\x00\x04\x00\x07\x00\x03\x06\xaf\x04\xbe\x10\x0d",
-            ),  # write file record
-            (0x16, b"\x16\x00\x01\x00\xff\xff\x00"),  # mask write register
-            (
-                0x17,
-                b"\x17\x00\x01\x00\x01\x00\x01\x00\x01\x02\x12\x34",
-            ),  # r/w multiple regs
-            (0x18, b"\x18\x00\x01"),  # read fifo queue
-            (0x2B, b"\x2b\x0e\x01\x00"),  # read device identification
-        )
-
-        self.response = (
-            (0x01, b"\x01\x01\x01"),  # read coils
-            (0x02, b"\x02\x01\x01"),  # read discrete inputs
-            (0x03, b"\x03\x02\x01\x01"),  # read holding registers
-            (0x04, b"\x04\x02\x01\x01"),  # read input registers
-            (0x05, b"\x05\x00\x01\x00\x01"),  # write single coil
-            (0x06, b"\x06\x00\x01\x00\x01"),  # write single register
-            (0x07, b"\x07\x00"),  # read exception status
-            (0x08, b"\x08\x00\x00\x00\x00"),  # read diagnostic
-            (0x0B, b"\x0b\x00\x00\x00\x00"),  # get comm event counters
-            (0x0C, b"\x0c\x08\x00\x00\x01\x08\x01\x21\x20\x00"),  # get comm event log
-            (0x0F, b"\x0f\x00\x01\x00\x08"),  # write multiple coils
-            (0x10, b"\x10\x00\x01\x00\x02"),  # write multiple registers
-            (0x11, b"\x11\x03\x05\x01\x54"),  # report slave id (device specific)
-            (
-                0x14,
-                b"\x14\x0c\x05\x06\x0d\xfe\x00\x20\x05\x06\x33\xcd\x00\x40",
-            ),  # read file record
-            (
-                0x15,
-                b"\x15\x0d\x06\x00\x04\x00\x07\x00\x03\x06\xaf\x04\xbe\x10\x0d",
-            ),  # write file record
-            (0x16, b"\x16\x00\x01\x00\xff\xff\x00"),  # mask write register
-            (0x17, b"\x17\x02\x12\x34"),  # read/write multiple registers
-            (0x18, b"\x18\x00\x01\x00\x01\x00\x00"),  # read fifo queue
-            (
-                0x2B,
-                b"\x2b\x0e\x01\x01\x00\x00\x01\x00\x01\x77",
-            ),  # read device identification
-        )
-
-        self.exception = (
-            (0x81, b"\x81\x01\xd0\x50"),  # illegal function exception
-            (0x82, b"\x82\x02\x90\xa1"),  # illegal data address exception
-            (0x83, b"\x83\x03\x50\xf1"),  # illegal data value exception
-            (0x84, b"\x84\x04\x13\x03"),  # skave device failure exception
-            (0x85, b"\x85\x05\xd3\x53"),  # acknowledge exception
-            (0x86, b"\x86\x06\x93\xa2"),  # slave device busy exception
-            (0x87, b"\x87\x08\x53\xf2"),  # memory parity exception
-            (0x88, b"\x88\x0a\x16\x06"),  # gateway path unavailable exception
-            (0x89, b"\x89\x0b\xd6\x56"),  # gateway target failed exception
-        )
-
-        self.bad = (
-            (0x80, b"\x80\x00\x00\x00"),  # Unknown Function
-            (0x81, b"\x81\x00\x00\x00"),  # error message
-        )
-
-    def tearDown(self):
-        """Clean up the test environment"""
-        del self.bad
-        del self.request
-        del self.response
 
     def test_exception_lookup(self):
         """Test that we can look up exception messages"""
         for func, _ in self.exception:
             response = self.client.lookupPduClass(func)
-            self.assertNotEqual(response, None)
-
-        for func, _ in self.exception:
-            response = self.server.lookupPduClass(func)
-            self.assertNotEqual(response, None)
+            assert response
 
     def test_response_lookup(self):
         """Test a working response factory lookup"""
         for func, _ in self.response:
             response = self.client.lookupPduClass(func)
-            self.assertNotEqual(response, None)
+            assert response
 
     def test_request_lookup(self):
         """Test a working request factory lookup"""
         for func, _ in self.request:
             request = self.client.lookupPduClass(func)
-            self.assertNotEqual(request, None)
+            assert request
 
     def test_response_working(self):
         """Test a working response factory decoders"""
-        for func, msg in self.response:
+        for _func, msg in self.response:
             self.client.decode(msg)
 
     def test_response_errors(self):
         """Test a response factory decoder exceptions"""
-        self.assertRaises(
-            ModbusException,
-            self.client._helper,  # pylint: disable=protected-access
-            self.bad[0][1],
-        )
-        self.assertEqual(
-            self.client.decode(self.bad[1][1]).function_code,
-            self.bad[1][0],
-            "Failed to decode error PDU",
-        )
+        with pytest.raises(ModbusException):
+            self.client._helper(self.bad[0][1])  # pylint: disable=protected-access
+        assert (
+            self.client.decode(self.bad[1][1]).function_code == self.bad[1][0]
+        ), "Failed to decode error PDU"
 
     def test_requests_working(self):
         """Test a working request factory decoders"""
-        for func, msg in self.request:
+        for _func, msg in self.request:
             self.server.decode(msg)
 
     def test_client_factory_fails(self):
         """Tests that a client factory will fail to decode a bad message"""
         self.client._helper = _raise_exception  # pylint: disable=protected-access
         actual = self.client.decode(None)
-        self.assertEqual(actual, None)
+        assert not actual
 
     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)
-        self.assertEqual(actual, None)
+        assert not actual
 
     def test_server_register_custom_request(self):
         """Test server register custom request."""
@@ -174,16 +163,16 @@ class NoCustomRequest:  # pylint: disable=too-few-public-methods
             function_code = 0xFF
 
         self.server.register(CustomRequest)
-        self.assertTrue(self.client.lookupPduClass(CustomRequest.function_code))
+        assert self.client.lookupPduClass(CustomRequest.function_code)
         CustomRequest.sub_function_code = 0xFF
         self.server.register(CustomRequest)
-        self.assertTrue(self.server.lookupPduClass(CustomRequest.function_code))
+        assert self.server.lookupPduClass(CustomRequest.function_code)
         try:
             func_raised = False
             self.server.register(NoCustomRequest)
         except MessageRegisterException:
             func_raised = True
-        self.assertTrue(func_raised)
+        assert func_raised
 
     def test_client_register_custom_response(self):
         """Test client register custom response."""
@@ -199,16 +188,16 @@ class NoCustomResponse:  # pylint: disable=too-few-public-methods
             function_code = 0xFF
 
         self.client.register(CustomResponse)
-        self.assertTrue(self.client.lookupPduClass(CustomResponse.function_code))
+        assert self.client.lookupPduClass(CustomResponse.function_code)
         CustomResponse.sub_function_code = 0xFF
         self.client.register(CustomResponse)
-        self.assertTrue(self.client.lookupPduClass(CustomResponse.function_code))
+        assert self.client.lookupPduClass(CustomResponse.function_code)
         try:
             func_raised = False
             self.client.register(NoCustomResponse)
         except MessageRegisterException:
             func_raised = True
-        self.assertTrue(func_raised)
+        assert func_raised
 
     # ---------------------------------------------------------------------------#
     #  I don't actually know what is supposed to be returned here, I assume that
@@ -219,9 +208,7 @@ def test_request_errors(self):
         """Test a request factory decoder exceptions"""
         for func, msg in self.bad:
             result = self.server.decode(msg)
-            self.assertEqual(result.ErrorCode, 1, "Failed to decode invalid requests")
-            self.assertEqual(
-                result.execute(None).function_code,
-                func,
-                "Failed to create correct response message",
-            )
+            assert result.ErrorCode == 1, "Failed to decode invalid requests"
+            assert (
+                result.execute(None).function_code == func
+            ), "Failed to create correct response message"
diff --git a/test/test_file_message.py b/test/test_file_message.py
index 49558bacf..0214bb54b 100644
--- a/test/test_file_message.py
+++ b/test/test_file_message.py
@@ -6,7 +6,6 @@
 * Read/Write Discretes
 * Read Coils
 """
-import unittest
 from test.conftest import MockContext
 
 from pymodbus.file_message import (
@@ -28,49 +27,35 @@
 # ---------------------------------------------------------------------------#
 
 
-class ModbusBitMessageTests(unittest.TestCase):
+class TestBitMessage:
     """Modbus bit message tests."""
 
-    # -----------------------------------------------------------------------#
-    #  Setup/TearDown
-    # -----------------------------------------------------------------------#
-
-    def setUp(self):
-        """Initialize the test environment and builds request/result encoding pairs."""
-
-    def tearDown(self):
-        """Clean up the test environment"""
-
-    # -----------------------------------------------------------------------#
-    #  Read Fifo Queue
-    # -----------------------------------------------------------------------#
-
     def test_read_fifo_queue_request_encode(self):
         """Test basic bit message encoding/decoding"""
         handle = ReadFifoQueueRequest(0x1234)
         result = handle.encode()
-        self.assertEqual(result, b"\x12\x34")
+        assert result == b"\x12\x34"
 
     def test_read_fifo_queue_request_decode(self):
         """Test basic bit message encoding/decoding"""
         handle = ReadFifoQueueRequest(0x0000)
         handle.decode(b"\x12\x34")
-        self.assertEqual(handle.address, 0x1234)
+        assert handle.address == 0x1234
 
     def test_read_fifo_queue_request(self):
         """Test basic bit message encoding/decoding"""
         context = MockContext()
         handle = ReadFifoQueueRequest(0x1234)
         result = handle.execute(context)
-        self.assertTrue(isinstance(result, ReadFifoQueueResponse))
+        assert isinstance(result, ReadFifoQueueResponse)
 
         handle.address = -1
         result = handle.execute(context)
-        self.assertEqual(ModbusExceptions.IllegalValue, result.exception_code)
+        assert ModbusExceptions.IllegalValue == result.exception_code
 
         handle.values = [0x00] * 33
         result = handle.execute(context)
-        self.assertEqual(ModbusExceptions.IllegalValue, result.exception_code)
+        assert ModbusExceptions.IllegalValue == result.exception_code
 
     def test_read_fifo_queue_request_error(self):
         """Test basic bit message encoding/decoding"""
@@ -78,27 +63,27 @@ def test_read_fifo_queue_request_error(self):
         handle = ReadFifoQueueRequest(0x1234)
         handle.values = [0x00] * 32
         result = handle.execute(context)
-        self.assertEqual(result.function_code, 0x98)
+        assert result.function_code == 0x98
 
     def test_read_fifo_queue_response_encode(self):
         """Test that the read fifo queue response can encode"""
         message = TEST_MESSAGE
         handle = ReadFifoQueueResponse([1, 2, 3, 4])
         result = handle.encode()
-        self.assertEqual(result, message)
+        assert result == message
 
     def test_read_fifo_queue_response_decode(self):
         """Test that the read fifo queue response can decode"""
         message = TEST_MESSAGE
         handle = ReadFifoQueueResponse([1, 2, 3, 4])
         handle.decode(message)
-        self.assertEqual(handle.values, [1, 2, 3, 4])
+        assert handle.values == [1, 2, 3, 4]
 
     def test_rtu_frame_size(self):
         """Test that the read fifo queue response can decode"""
         message = TEST_MESSAGE
         result = ReadFifoQueueResponse.calculateRtuFrameSize(message)
-        self.assertEqual(result, 14)
+        assert result == 14
 
     # -----------------------------------------------------------------------#
     #  File Record
@@ -109,8 +94,8 @@ def test_file_record_length(self):
         record = FileRecord(
             file_number=0x01, record_number=0x02, record_data=b"\x00\x01\x02\x04"
         )
-        self.assertEqual(record.record_length, 0x02)
-        self.assertEqual(record.response_length, 0x05)
+        assert record.record_length == 0x02
+        assert record.response_length == 0x05
 
     def test_file_record_compare(self):
         """Test file record comparison operations"""
@@ -126,15 +111,15 @@ def test_file_record_compare(self):
         record4 = FileRecord(
             file_number=0x01, record_number=0x02, record_data=b"\x00\x01\x02\x04"
         )
-        self.assertTrue(record1 == record4)
-        self.assertTrue(record1 != record2)
-        self.assertNotEqual(record1, record2)
-        self.assertNotEqual(record1, record3)
-        self.assertNotEqual(record2, record3)
-        self.assertEqual(record1, record4)
-        self.assertEqual(str(record1), "FileRecord(file=1, record=2, length=2)")
-        self.assertEqual(str(record2), "FileRecord(file=1, record=2, length=2)")
-        self.assertEqual(str(record3), "FileRecord(file=2, record=3, length=2)")
+        assert record1 == record4
+        assert record1 != record2
+        assert record1 != record2
+        assert record1 != record3
+        assert record2 != record3
+        assert record1 == record4
+        assert str(record1) == "FileRecord(file=1, record=2, length=2)"
+        assert str(record2) == "FileRecord(file=1, record=2, length=2)"
+        assert str(record3) == "FileRecord(file=2, record=3, length=2)"
 
     # -----------------------------------------------------------------------#
     #  Read File Record Request
@@ -145,7 +130,7 @@ def test_read_file_record_request_encode(self):
         records = [FileRecord(file_number=0x01, record_number=0x02)]
         handle = ReadFileRecordRequest(records)
         result = handle.encode()
-        self.assertEqual(result, b"\x07\x06\x00\x01\x00\x02\x00\x00")
+        assert result == b"\x07\x06\x00\x01\x00\x02\x00\x00"
 
     def test_read_file_record_request_decode(self):
         """Test basic bit message encoding/decoding"""
@@ -153,7 +138,7 @@ def test_read_file_record_request_decode(self):
         request = b"\x0e\x06\x00\x04\x00\x01\x00\x02\x06\x00\x03\x00\x09\x00\x02"
         handle = ReadFileRecordRequest()
         handle.decode(request)
-        self.assertEqual(handle.records[0], record)
+        assert handle.records[0] == record
 
     def test_read_file_record_request_rtu_frame_size(self):
         """Test basic bit message encoding/decoding"""
@@ -162,13 +147,13 @@ def test_read_file_record_request_rtu_frame_size(self):
         )
         handle = ReadFileRecordRequest()
         size = handle.calculateRtuFrameSize(request)
-        self.assertEqual(size, 0x0E + 5)
+        assert size == 0x0E + 5
 
     def test_read_file_record_request_execute(self):
         """Test basic bit message encoding/decoding"""
         handle = ReadFileRecordRequest()
         result = handle.execute(None)
-        self.assertTrue(isinstance(result, ReadFileRecordResponse))
+        assert isinstance(result, ReadFileRecordResponse)
 
     # -----------------------------------------------------------------------#
     #  Read File Record Response
@@ -179,7 +164,7 @@ def test_read_file_record_response_encode(self):
         records = [FileRecord(record_data=b"\x00\x01\x02\x03")]
         handle = ReadFileRecordResponse(records)
         result = handle.encode()
-        self.assertEqual(result, b"\x06\x06\x02\x00\x01\x02\x03")
+        assert result == b"\x06\x06\x02\x00\x01\x02\x03"
 
     def test_read_file_record_response_decode(self):
         """Test basic bit message encoding/decoding"""
@@ -189,14 +174,14 @@ def test_read_file_record_response_decode(self):
         request = b"\x0c\x05\x06\x0d\xfe\x00\x20\x05\x05\x06\x33\xcd\x00\x40"
         handle = ReadFileRecordResponse()
         handle.decode(request)
-        self.assertEqual(handle.records[0], record)
+        assert handle.records[0] == record
 
     def test_read_file_record_response_rtu_frame_size(self):
         """Test basic bit message encoding/decoding"""
         request = b"\x00\x00\x0c\x05\x06\x0d\xfe\x00\x20\x05\x05\x06\x33\xcd\x00\x40"
         handle = ReadFileRecordResponse()
         size = handle.calculateRtuFrameSize(request)
-        self.assertEqual(size, 0x0C + 5)
+        assert size == 0x0C + 5
 
     # -----------------------------------------------------------------------#
     #  Write File Record Request
@@ -211,7 +196,7 @@ def test_write_file_record_request_encode(self):
         ]
         handle = WriteFileRecordRequest(records)
         result = handle.encode()
-        self.assertEqual(result, b"\x0b\x06\x00\x01\x00\x02\x00\x02\x00\x01\x02\x03")
+        assert result == b"\x0b\x06\x00\x01\x00\x02\x00\x02\x00\x01\x02\x03"
 
     def test_write_file_record_request_decode(self):
         """Test basic bit message encoding/decoding"""
@@ -223,20 +208,20 @@ def test_write_file_record_request_decode(self):
         request = b"\x0d\x06\x00\x04\x00\x07\x00\x03\x06\xaf\x04\xbe\x10\x0d"
         handle = WriteFileRecordRequest()
         handle.decode(request)
-        self.assertEqual(handle.records[0], record)
+        assert handle.records[0] == record
 
     def test_write_file_record_request_rtu_frame_size(self):
         """Test write file record request rtu frame size calculation"""
         request = b"\x00\x00\x0d\x06\x00\x04\x00\x07\x00\x03\x06\xaf\x04\xbe\x10\x0d"
         handle = WriteFileRecordRequest()
         size = handle.calculateRtuFrameSize(request)
-        self.assertEqual(size, 0x0D + 5)
+        assert size == 0x0D + 5
 
     def test_write_file_record_request_execute(self):
         """Test basic bit message encoding/decoding"""
         handle = WriteFileRecordRequest()
         result = handle.execute(None)
-        self.assertTrue(isinstance(result, WriteFileRecordResponse))
+        assert isinstance(result, WriteFileRecordResponse)
 
     # -----------------------------------------------------------------------#
     #  Write File Record Response
@@ -251,7 +236,7 @@ def test_write_file_record_response_encode(self):
         ]
         handle = WriteFileRecordResponse(records)
         result = handle.encode()
-        self.assertEqual(result, b"\x0b\x06\x00\x01\x00\x02\x00\x02\x00\x01\x02\x03")
+        assert result == b"\x0b\x06\x00\x01\x00\x02\x00\x02\x00\x01\x02\x03"
 
     def test_write_file_record_response_decode(self):
         """Test basic bit message encoding/decoding"""
@@ -263,11 +248,11 @@ def test_write_file_record_response_decode(self):
         request = b"\x0d\x06\x00\x04\x00\x07\x00\x03\x06\xaf\x04\xbe\x10\x0d"
         handle = WriteFileRecordResponse()
         handle.decode(request)
-        self.assertEqual(handle.records[0], record)
+        assert handle.records[0] == record
 
     def test_write_file_record_response_rtu_frame_size(self):
         """Test write file record response rtu frame size calculation"""
         request = b"\x00\x00\x0d\x06\x00\x04\x00\x07\x00\x03\x06\xaf\x04\xbe\x10\x0d"
         handle = WriteFileRecordResponse()
         size = handle.calculateRtuFrameSize(request)
-        self.assertEqual(size, 0x0D + 5)
+        assert size == 0x0D + 5
diff --git a/test/test_framers.py b/test/test_framers.py
index 2132088da..3f7b55702 100644
--- a/test/test_framers.py
+++ b/test/test_framers.py
@@ -1,5 +1,5 @@
 """Test framers."""
-from unittest.mock import Mock, patch
+from unittest import mock
 
 import pytest
 
@@ -16,14 +16,14 @@
 TEST_MESSAGE = b"\x00\x01\x00\x01\x00\n\xec\x1c"
 
 
-@pytest.fixture
-def rtu_framer():
+@pytest.fixture(name="rtu_framer")
+def fixture_rtu_framer():
     """RTU framer."""
     return ModbusRtuFramer(ClientDecoder())
 
 
-@pytest.fixture
-def ascii_framer():
+@pytest.fixture(name="ascii_framer")
+def fixture_ascii_framer():
     """Ascii framer."""
     return ModbusAsciiFramer(ClientDecoder())
 
@@ -82,8 +82,8 @@ def test_framer_initialization(framer):
         ]
 
 
-@pytest.mark.parametrize("data", [(b"", {}), (b"abcd", {"fcode": 98, "unit": 97})])
-def test_decode_data(rtu_framer, data):  # pylint: disable=redefined-outer-name
+@pytest.mark.parametrize("data", [(b"", {}), (b"abcd", {"fcode": 98, "slave": 97})])
+def test_decode_data(rtu_framer, data):
     """Test decode data."""
     data, expected = data
     decoded = rtu_framer.decode_data(data)
@@ -99,7 +99,7 @@ def test_decode_data(rtu_framer, data):  # pylint: disable=redefined-outer-name
         (b"\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAC", False),  # invalid frame CRC
     ],
 )
-def test_check_frame(rtu_framer, data):  # pylint: disable=redefined-outer-name
+def test_check_frame(rtu_framer, data):
     """Test check frame."""
     data, expected = data
     rtu_framer._buffer = data  # pylint: disable=protected-access
@@ -118,7 +118,7 @@ def test_check_frame(rtu_framer, data):  # pylint: disable=redefined-outer-name
         ),
     ],
 )
-def test_rtu_advance_framer(rtu_framer, data):  # pylint: disable=redefined-outer-name
+def test_rtu_advance_framer(rtu_framer, data):
     """Test rtu advance framer."""
     before_buf, before_header, after_buf = data
 
@@ -134,7 +134,7 @@ def test_rtu_advance_framer(rtu_framer, data):  # pylint: disable=redefined-oute
 
 
 @pytest.mark.parametrize("data", [b"", b"abcd"])
-def test_rtu_reset_framer(rtu_framer, data):  # pylint: disable=redefined-outer-name
+def test_rtu_reset_framer(rtu_framer, data):
     """Test rtu reset framer."""
     rtu_framer._buffer = data  # pylint: disable=protected-access
     rtu_framer.resetFrame()
@@ -143,7 +143,6 @@ def test_rtu_reset_framer(rtu_framer, data):  # pylint: disable=redefined-outer-
         "len": 0,
         "crc": b"\x00\x00",
     }
-    assert rtu_framer._buffer == b""  # pylint: disable=protected-access
 
 
 @pytest.mark.parametrize(
@@ -158,7 +157,7 @@ def test_rtu_reset_framer(rtu_framer, data):  # pylint: disable=redefined-outer-
         (b"\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAD\xAB\xCD", True),
     ],
 )
-def test_is_frame_ready(rtu_framer, data):  # pylint: disable=redefined-outer-name
+def test_is_frame_ready(rtu_framer, data):
     """Test is frame ready."""
     data, expected = data
     rtu_framer._buffer = data  # pylint: disable=protected-access
@@ -176,9 +175,7 @@ def test_is_frame_ready(rtu_framer, data):  # pylint: disable=redefined-outer-na
         b"\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x43",
     ],
 )
-def test_rtu_populate_header_fail(
-    rtu_framer, data
-):  # pylint: disable=redefined-outer-name
+def test_rtu_populate_header_fail(rtu_framer, data):
     """Test rtu populate header fail."""
     with pytest.raises(IndexError):
         rtu_framer.populateHeader(data)
@@ -197,33 +194,33 @@ def test_rtu_populate_header_fail(
         ),
     ],
 )
-def test_rtu_populate_header(rtu_framer, data):  # pylint: disable=redefined-outer-name
+def test_rtu_populate_header(rtu_framer, data):
     """Test rtu populate header."""
     buffer, expected = data
     rtu_framer.populateHeader(buffer)
     assert rtu_framer._header == expected  # pylint: disable=protected-access
 
 
-def test_add_to_frame(rtu_framer):  # pylint: disable=redefined-outer-name
+def test_add_to_frame(rtu_framer):
     """Test add to frame."""
     assert rtu_framer._buffer == b""  # pylint: disable=protected-access
     rtu_framer.addToFrame(b"abcd")
     assert rtu_framer._buffer == b"abcd"  # pylint: disable=protected-access
 
 
-def test_get_frame(rtu_framer):  # pylint: disable=redefined-outer-name
+def test_get_frame(rtu_framer):
     """Test get frame."""
     rtu_framer.addToFrame(b"\x02\x01\x01\x00Q\xcc")
     rtu_framer.populateHeader(b"\x02\x01\x01\x00Q\xcc")
     assert rtu_framer.getFrame() == b"\x01\x01\x00"
 
 
-def test_populate_result(rtu_framer):  # pylint: disable=redefined-outer-name
+def test_populate_result(rtu_framer):
     """Test populate result."""
     rtu_framer._header["uid"] = 255  # pylint: disable=protected-access
-    result = Mock()
+    result = mock.Mock()
     rtu_framer.populateResult(result)
-    assert result.unit_id == 255
+    assert result.slave_id == 255
 
 
 @pytest.mark.parametrize(
@@ -255,36 +252,36 @@ def test_populate_result(rtu_framer):  # pylint: disable=redefined-outer-name
         (
             b"\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAD",
             16,
-            True,
             False,
-        ),  # incorrect unit id
+            False,
+        ),  # incorrect slave id
         (b"\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAD\x11\x03", 17, False, True),
         # good frame + part of next frame
     ],
 )
-def test_rtu_incoming_packet(rtu_framer, data):  # pylint: disable=redefined-outer-name
+def test_rtu_incoming_packet(rtu_framer, data):
     """Test rtu process incoming packet."""
-    buffer, units, reset_called, process_called = data
+    buffer, slaves, reset_called, process_called = data
 
-    with patch.object(
+    with mock.patch.object(
         rtu_framer,
         "_process",
         wraps=rtu_framer._process,  # pylint: disable=protected-access
-    ) as mock_process, patch.object(
+    ) as mock_process, mock.patch.object(
         rtu_framer, "resetFrame", wraps=rtu_framer.resetFrame
     ) as mock_reset:
-        rtu_framer.processIncomingPacket(buffer, Mock(), units)
+        rtu_framer.processIncomingPacket(buffer, mock.Mock(), slaves)
         assert mock_process.call_count == (1 if process_called else 0)
         assert mock_reset.call_count == (1 if reset_called else 0)
 
 
-def test_build_packet(rtu_framer):  # pylint: disable=redefined-outer-name
+def test_build_packet(rtu_framer):
     """Test build packet."""
     message = ReadCoilsRequest(1, 10)
     assert rtu_framer.buildPacket(message) == TEST_MESSAGE
 
 
-def test_send_packet(rtu_framer):  # pylint: disable=redefined-outer-name
+def test_send_packet(rtu_framer):
     """Test send packet."""
     message = TEST_MESSAGE
     client = ModbusBaseClient(framer=ModbusRtuFramer)
@@ -292,24 +289,24 @@ def test_send_packet(rtu_framer):  # pylint: disable=redefined-outer-name
     client.silent_interval = 1
     client.last_frame_end = 1
     client.params.timeout = 0.25
-    client.idle_time = Mock(return_value=1)
-    client.send = Mock(return_value=len(message))
+    client.idle_time = mock.Mock(return_value=1)
+    client.send = mock.Mock(return_value=len(message))
     rtu_framer.client = client
     assert rtu_framer.sendPacket(message) == len(message)
     client.state = ModbusTransactionState.PROCESSING_REPLY
     assert rtu_framer.sendPacket(message) == len(message)
 
 
-def test_recv_packet(rtu_framer):  # pylint: disable=redefined-outer-name
+def test_recv_packet(rtu_framer):
     """Test receive packet."""
     message = TEST_MESSAGE
-    client = Mock()
+    client = mock.Mock()
     client.recv.return_value = message
     rtu_framer.client = client
     assert rtu_framer.recvPacket(len(message)) == message
 
 
-def test_process(rtu_framer):  # pylint: disable=redefined-outer-name
+def test_process(rtu_framer):
     """Test process."""
 
     rtu_framer._buffer = TEST_MESSAGE  # pylint: disable=protected-access
@@ -317,7 +314,7 @@ def test_process(rtu_framer):  # pylint: disable=redefined-outer-name
         rtu_framer._process(None)  # pylint: disable=protected-access
 
 
-def test_get_raw_frame(rtu_framer):  # pylint: disable=redefined-outer-name
+def test_get_raw_frame(rtu_framer):
     """Test get raw frame."""
     rtu_framer._buffer = TEST_MESSAGE  # pylint: disable=protected-access
     assert (
@@ -326,20 +323,20 @@ def test_get_raw_frame(rtu_framer):  # pylint: disable=redefined-outer-name
     )
 
 
-def test_validate_unit_id(rtu_framer):  # pylint: disable=redefined-outer-name
-    """Test validate unit."""
+def test_validate__slave_id(rtu_framer):
+    """Test validate slave."""
     rtu_framer.populateHeader(TEST_MESSAGE)
-    assert rtu_framer._validate_unit_id([0], False)  # pylint: disable=protected-access
-    assert rtu_framer._validate_unit_id([1], True)  # pylint: disable=protected-access
+    assert rtu_framer._validate_slave_id([0], False)  # pylint: disable=protected-access
+    assert rtu_framer._validate_slave_id([1], True)  # pylint: disable=protected-access
 
 
 @pytest.mark.parametrize("data", [b":010100010001FC\r\n", b""])
-def test_decode_ascii_data(ascii_framer, data):  # pylint: disable=redefined-outer-name
+def test_decode_ascii_data(ascii_framer, data):
     """Test decode ascii."""
     data = ascii_framer.decode_data(data)
     assert isinstance(data, dict)
     if data:
-        assert data.get("unit") == 1
+        assert data.get("slave") == 1
         assert data.get("fcode") == 1
     else:
         assert not data
diff --git a/test/test_logging.py b/test/test_logging.py
index b1c5f911f..7c12f015a 100644
--- a/test/test_logging.py
+++ b/test/test_logging.py
@@ -1,6 +1,6 @@
 """Test datastore."""
 import logging
-from unittest.mock import patch
+from unittest import mock
 
 import pytest
 
@@ -12,7 +12,7 @@ class TestLogging:
 
     def test_log_dont_call_build_msg(self):
         """Verify that build_msg is not called unnecessary"""
-        with patch("pymodbus.logging.Log.build_msg") as build_msg_mock:
+        with mock.patch("pymodbus.logging.Log.build_msg") as build_msg_mock:
             Log.setLevel(logging.INFO)
             Log.debug("test")
             build_msg_mock.assert_not_called()
@@ -28,7 +28,7 @@ def test_log_simple(self):
         assert log_txt == txt
 
     @pytest.mark.parametrize(
-        "txt, result, params",
+        ("txt", "result", "params"),
         [
             ("string {} {} {}", "string 101 102 103", (101, 102, 103)),
             ("string {}", "string 0x41 0x42 0x43 0x44", (b"ABCD", ":hex")),
diff --git a/test/test_mei_messages.py b/test/test_mei_messages.py
index f7131fac1..b1dbab820 100644
--- a/test/test_mei_messages.py
+++ b/test/test_mei_messages.py
@@ -3,7 +3,7 @@
 This fixture tests the functionality of all the
 mei based request/response messages:
 """
-import unittest
+import pytest
 
 from pymodbus.constants import DeviceInformation
 from pymodbus.device import ModbusControlBlock
@@ -21,7 +21,7 @@
 TEST_MESSAGE = b"\x00\x07Company\x01\x07Product\x02\x07v2.1.12"
 
 
-class ModbusMeiMessageTest(unittest.TestCase):
+class TestMeiMessage:
     """Unittest for the pymodbus.mei_message module."""
 
     # -----------------------------------------------------------------------#
@@ -33,15 +33,15 @@ def test_read_device_information_request_encode(self):
         params = {"read_code": DeviceInformation.Basic, "object_id": 0x00}
         handle = ReadDeviceInformationRequest(**params)
         result = handle.encode()
-        self.assertEqual(result, b"\x0e\x01\x00")
-        self.assertEqual("ReadDeviceInformationRequest(1,0)", str(handle))
+        assert result == b"\x0e\x01\x00"
+        assert str(handle) == "ReadDeviceInformationRequest(1,0)"
 
     def test_read_device_information_request_decode(self):
         """Test basic bit message encoding/decoding"""
         handle = ReadDeviceInformationRequest()
         handle.decode(b"\x0e\x01\x00")
-        self.assertEqual(handle.read_code, DeviceInformation.Basic)
-        self.assertEqual(handle.object_id, 0x00)
+        assert handle.read_code == DeviceInformation.Basic
+        assert not handle.object_id
 
     def test_read_device_information_request(self):
         """Test basic bit message encoding/decoding"""
@@ -54,30 +54,30 @@ def test_read_device_information_request(self):
 
         handle = ReadDeviceInformationRequest()
         result = handle.execute(context)
-        self.assertTrue(isinstance(result, ReadDeviceInformationResponse))
-        self.assertEqual(result.information[0x00], "Company")
-        self.assertEqual(result.information[0x01], "Product")
-        self.assertEqual(result.information[0x02], TEST_VERSION)
-        with self.assertRaises(KeyError):
+        assert isinstance(result, ReadDeviceInformationResponse)
+        assert result.information[0x00] == "Company"
+        assert result.information[0x01] == "Product"
+        assert result.information[0x02] == TEST_VERSION
+        with pytest.raises(KeyError):
             _ = result.information[0x81]
 
         handle = ReadDeviceInformationRequest(
             read_code=DeviceInformation.Extended, object_id=0x80
         )
         result = handle.execute(context)
-        self.assertEqual(result.information[0x81], ["Test", "Repeated"])
+        assert result.information[0x81] == ["Test", "Repeated"]
 
     def test_read_device_information_request_error(self):
         """Test basic bit message encoding/decoding"""
         handle = ReadDeviceInformationRequest()
         handle.read_code = -1
-        self.assertEqual(handle.execute(None).function_code, 0xAB)
+        assert handle.execute(None).function_code == 0xAB
         handle.read_code = 0x05
-        self.assertEqual(handle.execute(None).function_code, 0xAB)
+        assert handle.execute(None).function_code == 0xAB
         handle.object_id = -1
-        self.assertEqual(handle.execute(None).function_code, 0xAB)
+        assert handle.execute(None).function_code == 0xAB
         handle.object_id = 0x100
-        self.assertEqual(handle.execute(None).function_code, 0xAB)
+        assert handle.execute(None).function_code == 0xAB
 
     def test_read_device_information_encode(self):
         """Test that the read fifo queue response can encode"""
@@ -92,8 +92,8 @@ def test_read_device_information_encode(self):
             read_code=DeviceInformation.Basic, information=dataset
         )
         result = handle.encode()
-        self.assertEqual(result, message)
-        self.assertEqual("ReadDeviceInformationResponse(1)", str(handle))
+        assert result == message
+        assert str(handle) == "ReadDeviceInformationResponse(1)"
 
         dataset = {
             0x00: "Company",
@@ -108,7 +108,7 @@ def test_read_device_information_encode(self):
             read_code=DeviceInformation.Extended, information=dataset
         )
         result = handle.encode()
-        self.assertEqual(result, message)
+        assert result == message
 
     def test_read_device_information_encode_long(self):
         """Test that the read fifo queue response can encode"""
@@ -132,8 +132,8 @@ def test_read_device_information_encode_long(self):
             read_code=DeviceInformation.Basic, information=dataset
         )
         result = handle.encode()
-        self.assertEqual(result, message)
-        self.assertEqual("ReadDeviceInformationResponse(1)", str(handle))
+        assert result == message
+        assert str(handle) == "ReadDeviceInformationResponse(1)"
 
     def test_read_device_information_decode(self):
         """Test that the read device information response can decode"""
@@ -142,12 +142,12 @@ def test_read_device_information_decode(self):
         message += b"\x81\x04Test\x81\x08Repeated\x81\x07Another"
         handle = ReadDeviceInformationResponse(read_code=0x00, information=[])
         handle.decode(message)
-        self.assertEqual(handle.read_code, DeviceInformation.Basic)
-        self.assertEqual(handle.conformity, 0x01)
-        self.assertEqual(handle.information[0x00], b"Company")
-        self.assertEqual(handle.information[0x01], b"Product")
-        self.assertEqual(handle.information[0x02], TEST_VERSION)
-        self.assertEqual(handle.information[0x81], [b"Test", b"Repeated", b"Another"])
+        assert handle.read_code == DeviceInformation.Basic
+        assert handle.conformity == 0x01
+        assert handle.information[0x00] == b"Company"
+        assert handle.information[0x01] == b"Product"
+        assert handle.information[0x02] == TEST_VERSION
+        assert handle.information[0x81] == [b"Test", b"Repeated", b"Another"]
 
     def test_rtu_frame_size(self):
         """Test that the read device information response can decode"""
@@ -155,7 +155,7 @@ def test_rtu_frame_size(self):
             b"\x04\x2B\x0E\x01\x81\x00\x01\x01\x00\x06\x66\x6F\x6F\x62\x61\x72\xD7\x3B"
         )
         result = ReadDeviceInformationResponse.calculateRtuFrameSize(message)
-        self.assertEqual(result, 18)
+        assert result == 18
         message = b"\x00\x2B\x0E\x02\x00\x4D\x47"
         result = ReadDeviceInformationRequest.calculateRtuFrameSize(message)
-        self.assertEqual(result, 7)
+        assert result == 7
diff --git a/test/test_other_messages.py b/test/test_other_messages.py
index fcc53217f..ed2174e20 100644
--- a/test/test_other_messages.py
+++ b/test/test_other_messages.py
@@ -1,96 +1,88 @@
 """Test other messages."""
-import unittest
 from unittest import mock
 
 import pymodbus.other_message as pymodbus_message
 
 
-class ModbusOtherMessageTest(unittest.TestCase):
+class TestOtherMessage:
     """Unittest for the pymodbus.other_message module."""
 
-    def setUp(self):
-        """Do setup."""
-        self.requests = [
-            pymodbus_message.ReadExceptionStatusRequest,
-            pymodbus_message.GetCommEventCounterRequest,
-            pymodbus_message.GetCommEventLogRequest,
-            pymodbus_message.ReportSlaveIdRequest,
-        ]
-
-        self.responses = [
-            lambda: pymodbus_message.ReadExceptionStatusResponse(0x12),
-            lambda: pymodbus_message.GetCommEventCounterResponse(0x12),
-            pymodbus_message.GetCommEventLogResponse,
-            lambda: pymodbus_message.ReportSlaveIdResponse(0x12),
-        ]
-
-    def tearDown(self):
-        """Clean up the test environment."""
-        del self.requests
-        del self.responses
+    requests = [
+        pymodbus_message.ReadExceptionStatusRequest,
+        pymodbus_message.GetCommEventCounterRequest,
+        pymodbus_message.GetCommEventLogRequest,
+        pymodbus_message.ReportSlaveIdRequest,
+    ]
+
+    responses = [
+        lambda: pymodbus_message.ReadExceptionStatusResponse(0x12),
+        lambda: pymodbus_message.GetCommEventCounterResponse(0x12),
+        pymodbus_message.GetCommEventLogResponse,
+        lambda: pymodbus_message.ReportSlaveIdResponse(0x12),
+    ]
 
     def test_other_messages_to_string(self):
         """Test other messages to string."""
         for message in self.requests:
-            self.assertNotEqual(str(message()), None)
+            assert str(message())
         for message in self.responses:
-            self.assertNotEqual(str(message()), None)
+            assert str(message())
 
     def test_read_exception_status(self):
         """Test read exception status."""
         request = pymodbus_message.ReadExceptionStatusRequest()
         request.decode(b"\x12")
-        self.assertEqual(request.encode(), b"")
-        self.assertEqual(request.execute().function_code, 0x07)
+        assert not request.encode()
+        assert request.execute().function_code == 0x07
 
         response = pymodbus_message.ReadExceptionStatusResponse(0x12)
-        self.assertEqual(response.encode(), b"\x12")
+        assert response.encode() == b"\x12"
         response.decode(b"\x12")
-        self.assertEqual(response.status, 0x12)
+        assert response.status == 0x12
 
     def test_get_comm_event_counter(self):
         """Test get comm event counter."""
         request = pymodbus_message.GetCommEventCounterRequest()
         request.decode(b"\x12")
-        self.assertEqual(request.encode(), b"")
-        self.assertEqual(request.execute().function_code, 0x0B)
+        assert not request.encode()
+        assert request.execute().function_code == 0x0B
 
         response = pymodbus_message.GetCommEventCounterResponse(0x12)
-        self.assertEqual(response.encode(), b"\x00\x00\x00\x12")
+        assert response.encode() == b"\x00\x00\x00\x12"
         response.decode(b"\x00\x00\x00\x12")
-        self.assertEqual(response.status, True)
-        self.assertEqual(response.count, 0x12)
+        assert response.status
+        assert response.count == 0x12
 
         response.status = False
-        self.assertEqual(response.encode(), b"\xFF\xFF\x00\x12")
+        assert response.encode() == b"\xFF\xFF\x00\x12"
 
     def test_get_comm_event_log(self):
         """Test get comm event log."""
         request = pymodbus_message.GetCommEventLogRequest()
         request.decode(b"\x12")
-        self.assertEqual(request.encode(), b"")
-        self.assertEqual(request.execute().function_code, 0x0C)
+        assert not request.encode()
+        assert request.execute().function_code == 0x0C
 
         response = pymodbus_message.GetCommEventLogResponse()
-        self.assertEqual(response.encode(), b"\x06\x00\x00\x00\x00\x00\x00")
+        assert response.encode() == b"\x06\x00\x00\x00\x00\x00\x00"
         response.decode(b"\x06\x00\x00\x00\x12\x00\x12")
-        self.assertEqual(response.status, True)
-        self.assertEqual(response.message_count, 0x12)
-        self.assertEqual(response.event_count, 0x12)
-        self.assertEqual(response.events, [])
+        assert response.status
+        assert response.message_count == 0x12
+        assert response.event_count == 0x12
+        assert not response.events
 
         response.status = False
-        self.assertEqual(response.encode(), b"\x06\xff\xff\x00\x12\x00\x12")
+        assert response.encode() == b"\x06\xff\xff\x00\x12\x00\x12"
 
     def test_get_comm_event_log_with_events(self):
         """Test get comm event log with events."""
         response = pymodbus_message.GetCommEventLogResponse(events=[0x12, 0x34, 0x56])
-        self.assertEqual(response.encode(), b"\x09\x00\x00\x00\x00\x00\x00\x12\x34\x56")
+        assert response.encode() == b"\x09\x00\x00\x00\x00\x00\x00\x12\x34\x56"
         response.decode(b"\x09\x00\x00\x00\x12\x00\x12\x12\x34\x56")
-        self.assertEqual(response.status, True)
-        self.assertEqual(response.message_count, 0x12)
-        self.assertEqual(response.event_count, 0x12)
-        self.assertEqual(response.events, [0x12, 0x34, 0x56])
+        assert response.status
+        assert response.message_count == 0x12
+        assert response.event_count == 0x12
+        assert response.events == [0x12, 0x34, 0x56]
 
     def test_report_slave_id_request(self):
         """Test report slave id request."""
@@ -112,7 +104,7 @@ def test_report_slave_id_request(self):
 
             request = pymodbus_message.ReportSlaveIdRequest()
             response = request.execute()
-            self.assertEqual(response.identifier, expected_identity)
+            assert response.identifier == expected_identity
 
             # Change to byte strings and test again (final result should be the same)
             identity = {
@@ -130,7 +122,7 @@ def test_report_slave_id_request(self):
 
             request = pymodbus_message.ReportSlaveIdRequest()
             response = request.execute()
-            self.assertEqual(response.identifier, expected_identity)
+            assert response.identifier == expected_identity
 
     def test_report_slave_id(self):
         """Test report slave id."""
@@ -138,17 +130,17 @@ def test_report_slave_id(self):
             dif.get.return_value = {}
             request = pymodbus_message.ReportSlaveIdRequest()
             request.decode(b"\x12")
-            self.assertEqual(request.encode(), b"")
-            self.assertEqual(request.execute().function_code, 0x11)
+            assert not request.encode()
+            assert request.execute().function_code == 0x11
 
             response = pymodbus_message.ReportSlaveIdResponse(
                 request.execute().identifier, True
             )
 
-            self.assertEqual(response.encode(), b"\tPymodbus\xff")
+            assert response.encode() == b"\tPymodbus\xff"
             response.decode(b"\x03\x12\x00")
-            self.assertEqual(response.status, False)
-            self.assertEqual(response.identifier, b"\x12\x00")
+            assert not response.status
+            assert response.identifier == b"\x12\x00"
 
             response.status = False
-            self.assertEqual(response.encode(), b"\x03\x12\x00\x00")
+            assert response.encode() == b"\x03\x12\x00\x00"
diff --git a/test/test_payload.py b/test/test_payload.py
index 93f64d929..272ad4a31 100644
--- a/test/test_payload.py
+++ b/test/test_payload.py
@@ -6,7 +6,7 @@
 * PayloadBuilder
 * PayloadDecoder
 """
-import unittest
+import pytest
 
 from pymodbus.constants import Endian
 from pymodbus.exceptions import ParameterException
@@ -18,35 +18,26 @@
 # ---------------------------------------------------------------------------#
 
 
-class ModbusPayloadUtilityTests(unittest.TestCase):
+class TestPayloadUtility:
     """Modbus payload utility tests."""
 
-    # ----------------------------------------------------------------------- #
-    # Setup/TearDown
-    # ----------------------------------------------------------------------- #
-
-    def setUp(self):
-        """Initialize the test environment and builds request/result encoding pairs."""
-        self.little_endian_payload = (
-            b"\x01\x02\x00\x03\x00\x00\x00\x04\x00\x00\x00\x00"
-            b"\x00\x00\x00\xff\xfe\xff\xfd\xff\xff\xff\xfc\xff"
-            b"\xff\xff\xff\xff\xff\xff\x00\x00\xa0\x3f\x00\x00"
-            b"\x00\x00\x00\x00\x19\x40\x01\x00\x74\x65\x73\x74"
-            b"\x11"
-        )
-
-        self.big_endian_payload = (
-            b"\x01\x00\x02\x00\x00\x00\x03\x00\x00\x00\x00\x00"
-            b"\x00\x00\x04\xff\xff\xfe\xff\xff\xff\xfd\xff\xff"
-            b"\xff\xff\xff\xff\xff\xfc\x3f\xa0\x00\x00\x40\x19"
-            b"\x00\x00\x00\x00\x00\x00\x00\x01\x74\x65\x73\x74"
-            b"\x11"
-        )
+    little_endian_payload = (
+        b"\x01\x02\x00\x03\x00\x00\x00\x04\x00\x00\x00\x00"
+        b"\x00\x00\x00\xff\xfe\xff\xfd\xff\xff\xff\xfc\xff"
+        b"\xff\xff\xff\xff\xff\xff\x00\x00\xa0\x3f\x00\x00"
+        b"\x00\x00\x00\x00\x19\x40\x01\x00\x74\x65\x73\x74"
+        b"\x11"
+    )
 
-        self.bitstring = [True, False, False, False, True, False, False, False]
+    big_endian_payload = (
+        b"\x01\x00\x02\x00\x00\x00\x03\x00\x00\x00\x00\x00"
+        b"\x00\x00\x04\xff\xff\xfe\xff\xff\xff\xfd\xff\xff"
+        b"\xff\xff\xff\xff\xff\xfc\x3f\xa0\x00\x00\x40\x19"
+        b"\x00\x00\x00\x00\x00\x00\x00\x01\x74\x65\x73\x74"
+        b"\x11"
+    )
 
-    def tearDown(self):
-        """Clean up the test environment"""
+    bitstring = [True, False, False, False, True, False, False, False]
 
     # ----------------------------------------------------------------------- #
     # Payload Builder Tests
@@ -66,9 +57,9 @@ def test_little_endian_payload_builder(self):
         builder.add_32bit_float(1.25)
         builder.add_64bit_float(6.25)
         builder.add_16bit_uint(1)  # placeholder
-        builder.add_string(b"test")
+        builder.add_string("test")
         builder.add_bits(self.bitstring)
-        self.assertEqual(self.little_endian_payload, builder.to_string())
+        assert self.little_endian_payload == builder.encode()
 
     def test_big_endian_payload_builder(self):
         """Test basic bit message encoding/decoding"""
@@ -86,7 +77,7 @@ def test_big_endian_payload_builder(self):
         builder.add_16bit_uint(1)  # placeholder
         builder.add_string("test")
         builder.add_bits(self.bitstring)
-        self.assertEqual(self.big_endian_payload, builder.to_string())
+        assert self.big_endian_payload == builder.encode()
 
     def test_payload_builder_reset(self):
         """Test basic bit message encoding/decoding"""
@@ -95,11 +86,11 @@ def test_payload_builder_reset(self):
         builder.add_8bit_uint(0x34)
         builder.add_8bit_uint(0x56)
         builder.add_8bit_uint(0x78)
-        self.assertEqual(b"\x12\x34\x56\x78", builder.to_string())
-        self.assertEqual([b"\x12\x34", b"\x56\x78"], builder.build())
+        assert builder.encode() == b"\x12\x34\x56\x78"
+        assert builder.build() == [b"\x12\x34", b"\x56\x78"]
         builder.reset()
-        self.assertEqual(b"", builder.to_string())
-        self.assertEqual([], builder.build())
+        assert not builder.encode()
+        assert not builder.build()
 
     def test_payload_builder_with_raw_payload(self):
         """Test basic bit message encoding/decoding"""
@@ -175,19 +166,19 @@ def test_payload_builder_with_raw_payload(self):
         builder = BinaryPayloadBuilder(
             [b"\x12", b"\x34", b"\x56", b"\x78"], repack=True
         )
-        self.assertEqual(b"\x12\x34\x56\x78", builder.to_string())
-        self.assertEqual([13330, 30806], builder.to_registers())
+        assert builder.encode() == b"\x12\x34\x56\x78"
+        assert builder.to_registers() == [13330, 30806]
         coils = builder.to_coils()
-        self.assertEqual(_coils1, coils)
+        assert _coils1 == coils
 
         builder = BinaryPayloadBuilder(
             [b"\x12", b"\x34", b"\x56", b"\x78"], byteorder=Endian.Big
         )
-        self.assertEqual(b"\x12\x34\x56\x78", builder.to_string())
-        self.assertEqual([4660, 22136], builder.to_registers())
-        self.assertEqual("\x12\x34\x56\x78", str(builder))
+        assert builder.encode() == b"\x12\x34\x56\x78"
+        assert builder.to_registers() == [4660, 22136]
+        assert str(builder) == "\x12\x34\x56\x78"
         coils = builder.to_coils()
-        self.assertEqual(_coils2, coils)
+        assert _coils2 == coils
 
     # ----------------------------------------------------------------------- #
     # Payload Decoder Tests
@@ -198,71 +189,68 @@ def test_little_endian_payload_decoder(self):
         decoder = BinaryPayloadDecoder(
             self.little_endian_payload, byteorder=Endian.Little, wordorder=Endian.Little
         )
-        self.assertEqual(1, decoder.decode_8bit_uint())
-        self.assertEqual(2, decoder.decode_16bit_uint())
-        self.assertEqual(3, decoder.decode_32bit_uint())
-        self.assertEqual(4, decoder.decode_64bit_uint())
-        self.assertEqual(-1, decoder.decode_8bit_int())
-        self.assertEqual(-2, decoder.decode_16bit_int())
-        self.assertEqual(-3, decoder.decode_32bit_int())
-        self.assertEqual(-4, decoder.decode_64bit_int())
-        self.assertEqual(1.25, decoder.decode_32bit_float())
-        self.assertEqual(6.25, decoder.decode_64bit_float())
-        self.assertEqual(None, decoder.skip_bytes(2))
-        self.assertEqual("test", decoder.decode_string(4).decode())
-        self.assertEqual(self.bitstring, decoder.decode_bits())
+        assert decoder.decode_8bit_uint() == 1
+        assert decoder.decode_16bit_uint() == 2
+        assert decoder.decode_32bit_uint() == 3
+        assert decoder.decode_64bit_uint() == 4
+        assert decoder.decode_8bit_int() == -1
+        assert decoder.decode_16bit_int() == -2
+        assert decoder.decode_32bit_int() == -3
+        assert decoder.decode_64bit_int() == -4
+        assert decoder.decode_32bit_float() == 1.25
+        assert decoder.decode_64bit_float() == 6.25
+        assert not decoder.skip_bytes(2)
+        assert decoder.decode_string(4).decode() == "test"
+        assert self.bitstring == decoder.decode_bits()
 
     def test_big_endian_payload_decoder(self):
         """Test basic bit message encoding/decoding"""
         decoder = BinaryPayloadDecoder(self.big_endian_payload, byteorder=Endian.Big)
-        self.assertEqual(1, decoder.decode_8bit_uint())
-        self.assertEqual(2, decoder.decode_16bit_uint())
-        self.assertEqual(3, decoder.decode_32bit_uint())
-        self.assertEqual(4, decoder.decode_64bit_uint())
-        self.assertEqual(-1, decoder.decode_8bit_int())
-        self.assertEqual(-2, decoder.decode_16bit_int())
-        self.assertEqual(-3, decoder.decode_32bit_int())
-        self.assertEqual(-4, decoder.decode_64bit_int())
-        self.assertEqual(1.25, decoder.decode_32bit_float())
-        self.assertEqual(6.25, decoder.decode_64bit_float())
-        self.assertEqual(None, decoder.skip_bytes(2))
-        self.assertEqual(b"test", decoder.decode_string(4))
-        self.assertEqual(self.bitstring, decoder.decode_bits())
+        assert decoder.decode_8bit_uint() == 1
+        assert decoder.decode_16bit_uint() == 2
+        assert decoder.decode_32bit_uint() == 3
+        assert decoder.decode_64bit_uint() == 4
+        assert decoder.decode_8bit_int() == -1
+        assert decoder.decode_16bit_int() == -2
+        assert decoder.decode_32bit_int() == -3
+        assert decoder.decode_64bit_int() == -4
+        assert decoder.decode_32bit_float() == 1.25
+        assert decoder.decode_64bit_float() == 6.25
+        assert not decoder.skip_bytes(2)
+        assert decoder.decode_string(4) == b"test"
+        assert self.bitstring == decoder.decode_bits()
 
     def test_payload_decoder_reset(self):
         """Test the payload decoder reset functionality"""
         decoder = BinaryPayloadDecoder(b"\x12\x34")
-        self.assertEqual(0x12, decoder.decode_8bit_uint())
-        self.assertEqual(0x34, decoder.decode_8bit_uint())
+        assert decoder.decode_8bit_uint() == 0x12
+        assert decoder.decode_8bit_uint() == 0x34
         decoder.reset()
-        self.assertEqual(0x3412, decoder.decode_16bit_uint())
+        assert decoder.decode_16bit_uint() == 0x3412
 
     def test_payload_decoder_register_factory(self):
         """Test the payload decoder reset functionality"""
         payload = [1, 2, 3, 4]
         decoder = BinaryPayloadDecoder.fromRegisters(payload, byteorder=Endian.Little)
         encoded = b"\x00\x01\x00\x02\x00\x03\x00\x04"
-        self.assertEqual(encoded, decoder.decode_string(8))
+        assert encoded == decoder.decode_string(8)
 
         decoder = BinaryPayloadDecoder.fromRegisters(payload, byteorder=Endian.Big)
         encoded = b"\x00\x01\x00\x02\x00\x03\x00\x04"
-        self.assertEqual(encoded, decoder.decode_string(8))
-
-        self.assertRaises(
-            ParameterException, lambda: BinaryPayloadDecoder.fromRegisters("abcd")
-        )
+        assert encoded == decoder.decode_string(8)
+        with pytest.raises(ParameterException):
+            BinaryPayloadDecoder.fromRegisters("abcd")
 
     def test_payload_decoder_coil_factory(self):
         """Test the payload decoder reset functionality"""
         payload = [1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1]
         decoder = BinaryPayloadDecoder.fromCoils(payload, byteorder=Endian.Little)
         encoded = b"\x88\x11"
-        self.assertEqual(encoded, decoder.decode_string(2))
+        assert encoded == decoder.decode_string(2)
 
         decoder = BinaryPayloadDecoder.fromCoils(payload, byteorder=Endian.Big)
         encoded = b"\x88\x11"
-        self.assertEqual(encoded, decoder.decode_string(2))
+        assert encoded == decoder.decode_string(2)
 
-        self.assertRaises(
-            ParameterException, lambda: BinaryPayloadDecoder.fromCoils("abcd")
-        )
+        with pytest.raises(ParameterException):
+            BinaryPayloadDecoder.fromCoils("abcd")
diff --git a/test/test_pdu.py b/test/test_pdu.py
index 8512c6eca..69021b09d 100644
--- a/test/test_pdu.py
+++ b/test/test_pdu.py
@@ -1,5 +1,5 @@
 """Test pdu."""
-import unittest
+import pytest
 
 from pymodbus.exceptions import NotImplementedException
 from pymodbus.pdu import (
@@ -11,31 +11,25 @@
 )
 
 
-class SimplePduTest(unittest.TestCase):
+class TestPdu:
     """Unittest for the pymod.pdu module."""
 
-    def setUp(self):
-        """Initialize the test environment"""
-        self.bad_requests = (
-            ModbusRequest(),
-            ModbusResponse(),
-        )
-        self.illegal = IllegalFunctionRequest(1)
-        self.exception = ExceptionResponse(1, 1)
-
-    def tearDown(self):
-        """Clean up the test environment"""
-        del self.bad_requests
-        del self.illegal
-        del self.exception
+    bad_requests = (
+        ModbusRequest(),
+        ModbusResponse(),
+    )
+    illegal = IllegalFunctionRequest(1)
+    exception = ExceptionResponse(1, 1)
 
     def test_not_impelmented(self):
         """Test a base classes for not implemented functions"""
         for request in self.bad_requests:
-            self.assertRaises(NotImplementedException, request.encode)
+            with pytest.raises(NotImplementedException):
+                request.encode()
 
         for request in self.bad_requests:
-            self.assertRaises(NotImplementedException, request.decode, None)
+            with pytest.raises(NotImplementedException):
+                request.decode(None)
 
     def test_error_methods(self):
         """Test all error methods"""
@@ -44,8 +38,8 @@ def test_error_methods(self):
 
         result = self.exception.encode()
         self.exception.decode(result)
-        self.assertEqual(result, b"\x01")
-        self.assertEqual(self.exception.exception_code, 1)
+        assert result == b"\x01"
+        assert self.exception.exception_code == 1
 
     def test_request_exception_factory(self):
         """Test all error methods"""
@@ -54,37 +48,35 @@ def test_request_exception_factory(self):
         errors = {ModbusExceptions.decode(c): c for c in range(1, 20)}
         for error, code in iter(errors.items()):
             result = request.doException(code)
-            self.assertEqual(str(result), f"Exception Response(129, 1, {error})")
+            assert str(result) == f"Exception Response(129, 1, {error})"
 
     def test_calculate_rtu_frame_size(self):
         """Test the calculation of Modbus/RTU frame sizes"""
-        self.assertRaises(
-            NotImplementedException, ModbusRequest.calculateRtuFrameSize, b""
-        )
+        with pytest.raises(NotImplementedException):
+            ModbusRequest.calculateRtuFrameSize(b"")
         ModbusRequest._rtu_frame_size = 5  # pylint: disable=protected-access
-        self.assertEqual(ModbusRequest.calculateRtuFrameSize(b""), 5)
+        assert ModbusRequest.calculateRtuFrameSize(b"") == 5
         del ModbusRequest._rtu_frame_size
 
         ModbusRequest._rtu_byte_count_pos = 2  # pylint: disable=protected-access
-        self.assertEqual(
+        assert (
             ModbusRequest.calculateRtuFrameSize(
                 b"\x11\x01\x05\xcd\x6b\xb2\x0e\x1b\x45\xe6"
-            ),
-            0x05 + 5,
+            )
+            == 0x05 + 5
         )
         del ModbusRequest._rtu_byte_count_pos
 
-        self.assertRaises(
-            NotImplementedException, ModbusResponse.calculateRtuFrameSize, b""
-        )
+        with pytest.raises(NotImplementedException):
+            ModbusResponse.calculateRtuFrameSize(b"")
         ModbusResponse._rtu_frame_size = 12  # pylint: disable=protected-access
-        self.assertEqual(ModbusResponse.calculateRtuFrameSize(b""), 12)
+        assert ModbusResponse.calculateRtuFrameSize(b"") == 12
         del ModbusResponse._rtu_frame_size
         ModbusResponse._rtu_byte_count_pos = 2  # pylint: disable=protected-access
-        self.assertEqual(
+        assert (
             ModbusResponse.calculateRtuFrameSize(
                 b"\x11\x01\x05\xcd\x6b\xb2\x0e\x1b\x45\xe6"
-            ),
-            0x05 + 5,
+            )
+            == 0x05 + 5
         )
         del ModbusResponse._rtu_byte_count_pos
diff --git a/test/test_register_read_messages.py b/test/test_register_read_messages.py
index 61c8df400..b9e19967a 100644
--- a/test/test_register_read_messages.py
+++ b/test/test_register_read_messages.py
@@ -1,5 +1,4 @@
 """Test register read messages."""
-import unittest
 from test.conftest import FakeList, MockContext
 
 from pymodbus.pdu import ModbusExceptions
@@ -22,7 +21,7 @@
 # ---------------------------------------------------------------------------#
 
 
-class ReadRegisterMessagesTest(unittest.TestCase):
+class TestReadRegisterMessages:
     """Register Message Test Fixture.
 
     This fixture tests the functionality of all the
@@ -32,7 +31,12 @@ class ReadRegisterMessagesTest(unittest.TestCase):
     * Read Holding Registers
     """
 
-    def setUp(self):
+    value = None
+    values = None
+    request_read = None
+    response_read = None
+
+    def setup_method(self):
         """Initialize the test environment and builds request/result encoding pairs."""
         arguments = {
             "read_address": 1,
@@ -64,26 +68,21 @@ def setUp(self):
             ReadWriteMultipleRegistersResponse(self.values): TEST_MESSAGE,
         }
 
-    def tearDown(self):
-        """Clean up the test environment."""
-        del self.request_read
-        del self.response_read
-
     def test_read_register_response_base(self):
         """Test read register response."""
         response = ReadRegistersResponseBase(list(range(10)))
         for index in range(10):
-            self.assertEqual(response.getRegister(index), index)
+            assert response.getRegister(index) == index
 
     def test_register_read_requests(self):
         """Test register read requests."""
         for request, response in iter(self.request_read.items()):
-            self.assertEqual(request.encode(), response)
+            assert request.encode() == response
 
     def test_register_read_responses(self):
         """Test register read response."""
         for request, response in iter(self.response_read.items()):
-            self.assertEqual(request.encode(), response)
+            assert request.encode() == response
 
     def test_register_read_response_decode(self):
         """Test register read response."""
@@ -100,7 +99,7 @@ def test_register_read_response_decode(self):
         for packet, register in zip(values, registers):
             request, response = packet
             request.decode(response)
-            self.assertEqual(request.registers, register)
+            assert request.registers == register
 
     def test_register_read_requests_count_errors(self):
         """This tests that the register request messages.
@@ -120,7 +119,7 @@ def test_register_read_requests_count_errors(self):
         ]
         for request in requests:
             result = request.execute(None)
-            self.assertEqual(ModbusExceptions.IllegalValue, result.exception_code)
+            assert ModbusExceptions.IllegalValue == result.exception_code
 
     def test_register_read_requests_validate_errors(self):
         """This tests that the register request messages.
@@ -136,7 +135,7 @@ def test_register_read_requests_validate_errors(self):
         ]
         for request in requests:
             result = request.execute(context)
-            self.assertEqual(ModbusExceptions.IllegalAddress, result.exception_code)
+            assert ModbusExceptions.IllegalAddress == result.exception_code
 
     def test_register_read_requests_execute(self):
         """This tests that the register request messages.
@@ -150,7 +149,7 @@ def test_register_read_requests_execute(self):
         ]
         for request in requests:
             response = request.execute(context)
-            self.assertEqual(request.function_code, response.function_code)
+            assert request.function_code == response.function_code
 
     def test_read_write_multiple_registers_request(self):
         """Test read/write multiple registers."""
@@ -159,7 +158,7 @@ def test_read_write_multiple_registers_request(self):
             read_address=1, read_count=10, write_address=1, write_registers=[0x00]
         )
         response = request.execute(context)
-        self.assertEqual(request.function_code, response.function_code)
+        assert request.function_code == response.function_code
 
     def test_read_write_multiple_registers_validate(self):
         """Test read/write multiple registers."""
@@ -169,15 +168,15 @@ def test_read_write_multiple_registers_validate(self):
             read_address=1, read_count=10, write_address=2, write_registers=[0x00]
         )
         response = request.execute(context)
-        self.assertEqual(response.exception_code, ModbusExceptions.IllegalAddress)
+        assert response.exception_code == ModbusExceptions.IllegalAddress
 
         context.validate = lambda f, a, c: a == 2
         response = request.execute(context)
-        self.assertEqual(response.exception_code, ModbusExceptions.IllegalAddress)
+        assert response.exception_code == ModbusExceptions.IllegalAddress
 
         request.write_byte_count = 0x100
         response = request.execute(context)
-        self.assertEqual(response.exception_code, ModbusExceptions.IllegalValue)
+        assert response.exception_code == ModbusExceptions.IllegalValue
 
     def test_read_write_multiple_registers_request_decode(self):
         """Test read/write multiple registers."""
@@ -187,16 +186,16 @@ def test_read_write_multiple_registers_request_decode(self):
             if getattr(k, "function_code", 0) == 23
         )
         request.decode(response)
-        self.assertEqual(request.read_address, 0x01)
-        self.assertEqual(request.write_address, 0x01)
-        self.assertEqual(request.read_count, 0x05)
-        self.assertEqual(request.write_count, 0x05)
-        self.assertEqual(request.write_byte_count, 0x0A)
-        self.assertEqual(request.write_registers, [0x00] * 5)
+        assert request.read_address == 0x01
+        assert request.write_address == 0x01
+        assert request.read_count == 0x05
+        assert request.write_count == 0x05
+        assert request.write_byte_count == 0x0A
+        assert request.write_registers == [0x00] * 5
 
     def test_serializing_to_string(self):
         """Test serializing to string."""
         for request in iter(self.request_read.keys()):
-            self.assertTrue(str(request) is not None)
+            assert str(request)
         for request in iter(self.response_read.keys()):
-            self.assertTrue(str(request) is not None)
+            assert str(request)
diff --git a/test/test_register_write_messages.py b/test/test_register_write_messages.py
index f19551046..b6e1c156f 100644
--- a/test/test_register_write_messages.py
+++ b/test/test_register_write_messages.py
@@ -1,5 +1,4 @@
 """Test register write messages."""
-import unittest
 from test.conftest import MockContext, MockLastValuesContext
 
 from pymodbus.payload import BinaryPayloadBuilder, Endian
@@ -19,7 +18,7 @@
 # ---------------------------------------------------------------------------#
 
 
-class WriteRegisterMessagesTest(unittest.TestCase):
+class TestWriteRegisterMessages:
     """Register Message Test Fixture.
 
     This fixture tests the functionality of all the
@@ -29,7 +28,13 @@ class WriteRegisterMessagesTest(unittest.TestCase):
     * Read Holding Registers
     """
 
-    def setUp(self):
+    value = None
+    values = None
+    builder = None
+    write = None
+    payload = None
+
+    def setup_method(self):
         """Initialize the test environment and builds request/result encoding pairs."""
         self.value = 0xABCD
         self.values = [0xA, 0xB, 0xC]
@@ -52,14 +57,10 @@ def setUp(self):
             ): b"\x00\x01\x00\x01\x02\x12\x34",
         }
 
-    def tearDown(self):
-        """Clean up the test environment"""
-        del self.write
-
     def test_register_write_requests_encode(self):
         """Test register write requests encode."""
         for request, response in iter(self.write.items()):
-            self.assertEqual(request.encode(), response)
+            assert request.encode() == response
 
     def test_register_write_requests_decode(self):
         """Test register write requests decode."""
@@ -71,56 +72,56 @@ def test_register_write_requests_decode(self):
         for packet, address in zip(values, addresses):
             request, response = packet
             request.decode(response)
-            self.assertEqual(request.address, address)
+            assert request.address == address
 
     def test_invalid_write_multiple_registers_request(self):
         """Test invalid write multiple registers request."""
         request = WriteMultipleRegistersRequest(0, None)
-        self.assertEqual(request.values, [])
+        assert not request.values
 
     def test_serializing_to_string(self):
         """Test serializing to string."""
         for request in iter(self.write.keys()):
-            self.assertTrue(str(request) is not None)
+            assert str(request)
 
     def test_write_single_register_request(self):
         """Test write single register request."""
         context = MockContext()
         request = WriteSingleRegisterRequest(0x00, 0xF0000)
         result = request.execute(context)
-        self.assertEqual(result.exception_code, ModbusExceptions.IllegalValue)
+        assert result.exception_code == ModbusExceptions.IllegalValue
 
         request.value = 0x00FF
         result = request.execute(context)
-        self.assertEqual(result.exception_code, ModbusExceptions.IllegalAddress)
+        assert result.exception_code == ModbusExceptions.IllegalAddress
 
         context.valid = True
         result = request.execute(context)
-        self.assertEqual(result.function_code, request.function_code)
+        assert result.function_code == request.function_code
 
     def test_write_multiple_register_request(self):
         """Test write multiple register request."""
         context = MockContext()
         request = WriteMultipleRegistersRequest(0x00, [0x00] * 10)
         result = request.execute(context)
-        self.assertEqual(result.exception_code, ModbusExceptions.IllegalAddress)
+        assert result.exception_code == ModbusExceptions.IllegalAddress
 
         request.count = 0x05  # bytecode != code * 2
         result = request.execute(context)
-        self.assertEqual(result.exception_code, ModbusExceptions.IllegalValue)
+        assert result.exception_code == ModbusExceptions.IllegalValue
 
         request.count = 0x800  # outside of range
         result = request.execute(context)
-        self.assertEqual(result.exception_code, ModbusExceptions.IllegalValue)
+        assert result.exception_code == ModbusExceptions.IllegalValue
 
         context.valid = True
         request = WriteMultipleRegistersRequest(0x00, [0x00] * 10)
         result = request.execute(context)
-        self.assertEqual(result.function_code, request.function_code)
+        assert result.function_code == request.function_code
 
         request = WriteMultipleRegistersRequest(0x00, 0x00)
         result = request.execute(context)
-        self.assertEqual(result.function_code, request.function_code)
+        assert result.function_code == request.function_code
 
         # -----------------------------------------------------------------------#
         # Mask Write Register Request
@@ -130,16 +131,16 @@ def test_mask_write_register_request_encode(self):
         """Test basic bit message encoding/decoding"""
         handle = MaskWriteRegisterRequest(0x0000, 0x0101, 0x1010)
         result = handle.encode()
-        self.assertEqual(result, b"\x00\x00\x01\x01\x10\x10")
+        assert result == b"\x00\x00\x01\x01\x10\x10"
 
     def test_mask_write_register_request_decode(self):
         """Test basic bit message encoding/decoding"""
         request = b"\x00\x04\x00\xf2\x00\x25"
         handle = MaskWriteRegisterRequest()
         handle.decode(request)
-        self.assertEqual(handle.address, 0x0004)
-        self.assertEqual(handle.and_mask, 0x00F2)
-        self.assertEqual(handle.or_mask, 0x0025)
+        assert handle.address == 0x0004
+        assert handle.and_mask == 0x00F2
+        assert handle.or_mask == 0x0025
 
     def test_mask_write_register_request_execute(self):
         """Test write register request valid execution"""
@@ -152,23 +153,23 @@ def test_mask_write_register_request_execute(self):
         context = MockLastValuesContext(valid=True, default=0xAA55)
         handle = MaskWriteRegisterRequest(0x0000, 0x0F0F, 0x00FF)
         result = handle.execute(context)
-        self.assertTrue(isinstance(result, MaskWriteRegisterResponse))
-        self.assertEqual([0x0AF5], context.last_values)
+        assert isinstance(result, MaskWriteRegisterResponse)
+        assert context.last_values == [0x0AF5]
 
     def test_mask_write_register_request_invalid_execute(self):
         """Test write register request execute with invalid data"""
         context = MockContext(valid=False, default=0x0000)
         handle = MaskWriteRegisterRequest(0x0000, -1, 0x1010)
         result = handle.execute(context)
-        self.assertEqual(ModbusExceptions.IllegalValue, result.exception_code)
+        assert ModbusExceptions.IllegalValue == result.exception_code
 
         handle = MaskWriteRegisterRequest(0x0000, 0x0101, -1)
         result = handle.execute(context)
-        self.assertEqual(ModbusExceptions.IllegalValue, result.exception_code)
+        assert ModbusExceptions.IllegalValue == result.exception_code
 
         handle = MaskWriteRegisterRequest(0x0000, 0x0101, 0x1010)
         result = handle.execute(context)
-        self.assertEqual(ModbusExceptions.IllegalAddress, result.exception_code)
+        assert ModbusExceptions.IllegalAddress == result.exception_code
 
         # -----------------------------------------------------------------------#
         # Mask Write Register Response
@@ -178,13 +179,13 @@ def test_mask_write_register_response_encode(self):
         """Test basic bit message encoding/decoding"""
         handle = MaskWriteRegisterResponse(0x0000, 0x0101, 0x1010)
         result = handle.encode()
-        self.assertEqual(result, b"\x00\x00\x01\x01\x10\x10")
+        assert result == b"\x00\x00\x01\x01\x10\x10"
 
     def test_mask_write_register_response_decode(self):
         """Test basic bit message encoding/decoding"""
         request = b"\x00\x04\x00\xf2\x00\x25"
         handle = MaskWriteRegisterResponse()
         handle.decode(request)
-        self.assertEqual(handle.address, 0x0004)
-        self.assertEqual(handle.and_mask, 0x00F2)
-        self.assertEqual(handle.or_mask, 0x0025)
+        assert handle.address == 0x0004
+        assert handle.and_mask == 0x00F2
+        assert handle.or_mask == 0x0025
diff --git a/test/test_remote_datastore.py b/test/test_remote_datastore.py
index b4ed047de..ec155d925 100644
--- a/test/test_remote_datastore.py
+++ b/test/test_remote_datastore.py
@@ -1,7 +1,8 @@
 """Test remote datastore."""
-import unittest
 from unittest import mock
 
+import pytest
+
 from pymodbus.bit_read_message import ReadCoilsResponse
 from pymodbus.bit_write_message import WriteMultipleCoilsResponse
 from pymodbus.datastore.remote import RemoteSlaveContext
@@ -10,17 +11,15 @@
 from pymodbus.register_read_message import ReadInputRegistersResponse
 
 
-class RemoteModbusDataStoreTest(unittest.TestCase):
+class TestRemoteDataStore:
     """Unittest for the pymodbus.datastore.remote module."""
 
     def test_remote_slave_context(self):
         """Test a modbus remote slave context"""
         context = RemoteSlaveContext(None)
-        self.assertNotEqual(str(context), None)
-        self.assertRaises(
-            NotImplementedException,
-            lambda: context.reset(),  # pylint: disable=unnecessary-lambda
-        )
+        assert str(context)
+        with pytest.raises(NotImplementedException):
+            context.reset()
 
     def test_remote_slave_set_values(self):
         """Test setting values against a remote slave context"""
@@ -40,15 +39,15 @@ def test_remote_slave_get_values(self):
         context = RemoteSlaveContext(client)
         context.validate(1, 0, 10)
         result = context.getValues(1, 0, 10)
-        self.assertEqual(result, [1] * 10)
+        assert result == [1] * 10
 
         context.validate(4, 0, 10)
         result = context.getValues(4, 0, 10)
-        self.assertEqual(result, [10] * 10)
+        assert result == [10] * 10
 
         context.validate(3, 0, 10)
         result = context.getValues(3, 0, 10)
-        self.assertNotEqual(result, [10] * 10)
+        assert result != [10] * 10
 
     def test_remote_slave_validate_values(self):
         """Test validating against a remote slave context"""
@@ -59,10 +58,10 @@ def test_remote_slave_validate_values(self):
 
         context = RemoteSlaveContext(client)
         result = context.validate(1, 0, 10)
-        self.assertTrue(result)
+        assert result
 
         result = context.validate(4, 0, 10)
-        self.assertTrue(result)
+        assert result
 
         result = context.validate(3, 0, 10)
-        self.assertFalse(result)
+        assert not result
diff --git a/test/test_repl_client.py b/test/test_repl_client.py
index 6a51acd48..5f39650f1 100755
--- a/test/test_repl_client.py
+++ b/test/test_repl_client.py
@@ -1,4 +1,6 @@
 """Test client sync."""
+from contextlib import suppress
+
 from pymodbus.repl.client.main import _process_args
 from pymodbus.server.reactive.default_config import DEFAULT_CONFIG
 
@@ -32,17 +34,11 @@ def test_repl_client_process_args():
     resp = _process_args(["address=0b11", "value=0x10"], False)
     assert resp == ({"address": 3, "value": 16}, True)
 
-    try:
+    with suppress(ValueError):
         resp = _process_args(["address=0xhj", "value=0x10"], False)
-    except ValueError:
-        pass
 
-    try:
+    with suppress(ValueError):
         resp = _process_args(["address=11ah", "value=0x10"], False)
-    except ValueError:
-        pass
 
-    try:
+    with suppress(ValueError):
         resp = _process_args(["address=0b12", "value=0x10"], False)
-    except ValueError:
-        pass
diff --git a/test/test_server_asyncio.py b/test/test_server_asyncio.py
index 9e279cbdd..2f2be1643 100755
--- a/test/test_server_asyncio.py
+++ b/test/test_server_asyncio.py
@@ -2,9 +2,9 @@
 import asyncio
 import logging
 import ssl
-import unittest
 from asyncio import CancelledError
-from unittest.mock import AsyncMock, Mock, patch
+from contextlib import suppress
+from unittest import mock
 
 import pytest
 
@@ -88,20 +88,26 @@ def clear(cls):
         BasicClient.my_protocol = None
 
 
-class AsyncioServerTest(
-    unittest.IsolatedAsyncioTestCase
-):  # pylint: disable=too-many-public-methods
+class TestAsyncioServer:  # pylint: disable=too-many-public-methods
     """Unittest for the pymodbus.server.asyncio module.
 
-    The scope of this unit test is the life-cycle management of the network
+    The scope of this test is the life-cycle management of the network
     connections and server objects.
 
-    This unittest suite does not attempt to test any of the underlying protocol details
+    This test suite does not attempt to test any of the underlying protocol details
     """
 
-    def __init__(self, name):
-        """Initialize."""
-        super().__init__(name)
+    server = None
+    task = None
+    loop = None
+    store = None
+    context = None
+    identity = None
+
+    @pytest.fixture(autouse=True)
+    async def _setup_teardown(self):
+        """Initialize the test environment by setting up a dummy store and context."""
+        self.loop = asyncio.get_running_loop()
         self.store = ModbusSlaveContext(
             di=ModbusSequentialDataBlock(0, [17] * 100),
             co=ModbusSequentialDataBlock(0, [17] * 100),
@@ -112,22 +118,9 @@ def __init__(self, name):
         self.identity = ModbusDeviceIdentification(
             info_name={"VendorName": "VendorName"}
         )
-        self.server = None
-        self.task = None
-        self.loop = None
-
-    # -----------------------------------------------------------------------#
-    #  Setup/TearDown
-    # -----------------------------------------------------------------------#
-    def setUp(self):
-        """Initialize the test environment by setting up a dummy store and context."""
-
-    async def asyncSetUp(self):
-        """Initialize the test environment by setting up a dummy store and context."""
-        self.loop = asyncio.get_running_loop()
+        yield
 
-    async def asyncTearDown(self):
-        """Clean up the test environment"""
+        # teardown
         if self.server is not None:
             await self.server.server_close()
             self.server = None
@@ -135,32 +128,23 @@ async def asyncTearDown(self):
             await asyncio.sleep(0.1)
             if not self.task.cancelled():
                 self.task.cancel()
-                try:
+                with suppress(CancelledError):
                     await self.task
-                except CancelledError:
-                    pass
                 self.task = None
-        self.context = ModbusServerContext(slaves=self.store, single=True)
         BasicClient.clear()
 
-    def tearDown(self):
-        """Clean up the test environment."""
-
     def handle_task(self, result):
         """Handle task exit."""
-        try:
+        with suppress(CancelledError):
             result = result.result()
-        except CancelledError:
-            pass
 
     async def start_server(
-        self, do_forever=True, do_defer=True, do_tls=False, do_udp=False, do_ident=False
+        self, do_forever=True, do_tls=False, do_udp=False, do_ident=False
     ):
         """Handle setup and control of tcp server."""
         args = {
             "context": self.context,
             "address": SERV_ADDR,
-            "defer_start": do_defer,
         }
         if do_ident:
             args["identity"] = self.identity
@@ -176,24 +160,24 @@ async def start_server(
             self.server = ModbusTcpServer(
                 self.context, ModbusSocketFramer, self.identity, SERV_ADDR
             )
-        self.assertIsNotNone(self.server)
+        assert self.server
         if do_forever:
             self.task = asyncio.create_task(self.server.serve_forever())
             self.task.add_done_callback(self.handle_task)
-            self.assertFalse(self.task.cancelled())
+            assert not self.task.cancelled()
             await asyncio.wait_for(self.server.serving, timeout=0.1)
             if not do_udp:
-                self.assertIsNotNone(self.server.server)
+                assert self.server.server
         elif not do_udp:  # pylint: disable=confusing-consecutive-elif
-            self.assertIsNone(self.server.server)
-        self.assertEqual(self.server.control.Identity.VendorName, "VendorName")
+            assert not self.server.server
+        assert self.server.control.Identity.VendorName == "VendorName"
         await asyncio.sleep(0.1)
 
     async def connect_server(self):
         """Handle connect to server"""
-        BasicClient.connected = self.loop.create_future()
-        BasicClient.done = self.loop.create_future()
-        BasicClient.eof = self.loop.create_future()
+        BasicClient.connected = asyncio.Future()
+        BasicClient.done = asyncio.Future()
+        BasicClient.eof = asyncio.Future()
         random_port = self.server.server.sockets[0].getsockname()[
             1
         ]  # get the random server port
@@ -221,39 +205,39 @@ async def test_async_start_server(self):
     async def test_async_tcp_server_serve_forever_twice(self):
         """Call on serve_forever() twice should result in a runtime error"""
         await self.start_server()
-        with self.assertRaises(RuntimeError):
+        with pytest.raises(RuntimeError):
             await self.server.serve_forever()
 
     async def test_async_tcp_server_receive_data(self):
         """Test data sent on socket is received by internals - doesn't not process data"""
         BasicClient.data = b"\x01\x00\x00\x00\x00\x06\x01\x03\x00\x00\x00\x19"
         await self.start_server()
-        with patch(
+        with mock.patch(
             "pymodbus.transaction.ModbusSocketFramer.processIncomingPacket",
-            new_callable=Mock,
+            new_callable=mock.Mock,
         ) as process:
             await self.connect_server()
             process.assert_called_once()
-            self.assertTrue(process.call_args[1]["data"] == BasicClient.data)
+            assert process.call_args[1]["data"] == BasicClient.data
 
     async def test_async_tcp_server_roundtrip(self):
         """Test sending and receiving data on tcp socket"""
         expected_response = b"\x01\x00\x00\x00\x00\x05\x01\x03\x02\x00\x11"
-        BasicClient.data = TEST_DATA  # unit 1, read register
+        BasicClient.data = TEST_DATA  # slave 1, read register
         await self.start_server()
         await self.connect_server()
         await asyncio.wait_for(BasicClient.done, timeout=0.1)
-        self.assertEqual(BasicClient.received_data, expected_response)
+        assert BasicClient.received_data, expected_response
 
     async def test_async_tcp_server_connection_lost(self):
         """Test tcp stream interruption"""
         await self.start_server()
         await self.connect_server()
-        self.assertEqual(len(self.server.active_connections), 1)
+        assert len(self.server.active_connections), 1
 
         BasicClient.transport.close()
         await asyncio.sleep(0.2)  # so we have to wait a bit
-        self.assertFalse(self.server.active_connections)
+        assert not self.server.active_connections
 
     async def test_async_tcp_server_close_active_connection(self):
         """Test server_close() while there are active TCP connections"""
@@ -266,22 +250,22 @@ async def test_async_tcp_server_close_active_connection(self):
         await self.server.server_close()
 
     async def test_async_tcp_server_no_slave(self):
-        """Test unknown slave unit exception"""
+        """Test unknown slave exception"""
         self.context = ModbusServerContext(
             slaves={0x01: self.store, 0x02: self.store}, single=False
         )
         BasicClient.data = b"\x01\x00\x00\x00\x00\x06\x05\x03\x00\x00\x00\x01"
         await self.start_server()
         await self.connect_server()
-        self.assertFalse(BasicClient.eof.done())
-        self.server.server_close()
+        assert not BasicClient.eof.done()
+        await self.server.server_close()
         self.server = None
 
     async def test_async_tcp_server_modbus_error(self):
         """Test sending garbage data on a TCP socket should drop the connection"""
         BasicClient.data = TEST_DATA
         await self.start_server()
-        with patch(
+        with mock.patch(
             "pymodbus.register_read_message.ReadHoldingRegistersRequest.execute",
             side_effect=NoSuchSlaveException,
         ):
@@ -293,31 +277,30 @@ async def test_async_tcp_server_modbus_error(self):
     # -----------------------------------------------------------------------#
     async def test_async_start_tls_server_no_loop(self):
         """Test that the modbus tls asyncio server starts correctly"""
-        with patch.object(ssl.SSLContext, "load_cert_chain"):
+        with mock.patch.object(ssl.SSLContext, "load_cert_chain"):
             await self.start_server(do_tls=True, do_forever=False, do_ident=True)
-            self.assertEqual(self.server.control.Identity.VendorName, "VendorName")
-            self.assertIsNotNone(self.server.sslctx)
+            assert self.server.control.Identity.VendorName == "VendorName"
+            assert self.server.sslctx
 
     async def test_async_start_tls_server(self):
         """Test that the modbus tls asyncio server starts correctly"""
-        with patch.object(ssl.SSLContext, "load_cert_chain"):
+        with mock.patch.object(ssl.SSLContext, "load_cert_chain"):
             await self.start_server(do_tls=True, do_ident=True)
-            self.assertEqual(self.server.control.Identity.VendorName, "VendorName")
-            self.assertIsNotNone(self.server.sslctx)
+            assert self.server.control.Identity.VendorName == "VendorName"
+            assert self.server.sslctx
 
     async def test_async_tls_server_serve_forever(self):
         """Test StartAsyncTcpServer serve_forever() method"""
-        with patch(
-            "asyncio.base_events.Server.serve_forever", new_callable=AsyncMock
-        ) as serve:
-            with patch.object(ssl.SSLContext, "load_cert_chain"):
-                await self.start_server(do_tls=True, do_forever=False)
-                await self.server.serve_forever()
-                serve.assert_awaited()
+        with mock.patch(
+            "asyncio.base_events.Server.serve_forever", new_callable=mock.AsyncMock
+        ) as serve, mock.patch.object(ssl.SSLContext, "load_cert_chain"):
+            await self.start_server(do_tls=True, do_forever=False)
+            await self.server.serve_forever()
+            serve.assert_awaited()
 
     async def test_async_tls_server_serve_forever_twice(self):
         """Call on serve_forever() twice should result in a runtime error"""
-        with patch.object(ssl.SSLContext, "load_cert_chain"):
+        with mock.patch.object(ssl.SSLContext, "load_cert_chain"):
             await self.start_server(do_tls=True)
             with pytest.raises(RuntimeError):
                 await self.server.serve_forever()
@@ -329,19 +312,19 @@ async def test_async_tls_server_serve_forever_twice(self):
     async def test_async_start_udp_server_no_loop(self):
         """Test that the modbus udp asyncio server starts correctly"""
         await self.start_server(do_udp=True, do_forever=False, do_ident=True)
-        self.assertEqual(self.server.control.Identity.VendorName, "VendorName")
-        self.assertIsNone(self.server.protocol)
+        assert self.server.control.Identity.VendorName == "VendorName"
+        assert not self.server.protocol
 
     async def test_async_start_udp_server(self):
         """Test that the modbus udp asyncio server starts correctly"""
         await self.start_server(do_udp=True, do_ident=True)
-        self.assertEqual(self.server.control.Identity.VendorName, "VendorName")
-        self.assertFalse(self.server.protocol is None)
+        assert self.server.control.Identity.VendorName == "VendorName"
+        assert self.server.protocol
 
     async def test_async_udp_server_serve_forever_start(self):
         """Test StartAsyncUdpServer serve_forever() method"""
-        with patch(
-            "asyncio.base_events.Server.serve_forever", new_callable=AsyncMock
+        with mock.patch(
+            "asyncio.base_events.Server.serve_forever", new_callable=mock.AsyncMock
         ) as serve:
             await self.start_server(do_forever=False, do_ident=True)
             await self.server.serve_forever()
@@ -350,32 +333,30 @@ async def test_async_udp_server_serve_forever_start(self):
     async def test_async_udp_server_serve_forever_close(self):
         """Test StarAsyncUdpServer serve_forever() method"""
         await self.start_server(do_udp=True)
-        self.assertTrue(asyncio.isfuture(self.server.on_connection_terminated))
-        self.assertFalse(self.server.on_connection_terminated.done())
-
+        assert asyncio.isfuture(self.server.on_connection_terminated)
+        assert not self.server.on_connection_terminated.done()
         await self.server.server_close()
-        # TBD self.assertTrue(self.server.is_closing())
         self.server = None
 
     async def test_async_udp_server_serve_forever_twice(self):
         """Call on serve_forever() twice should result in a runtime error"""
         await self.start_server(do_udp=True, do_ident=True)
-        with self.assertRaises(RuntimeError):
+        with pytest.raises(RuntimeError):
             await self.server.serve_forever()
 
     @pytest.mark.skipif(pytest.IS_WINDOWS, reason="Windows have a timeout problem.")
     async def test_async_udp_server_receive_data(self):
         """Test that the sending data on datagram socket gets data pushed to framer"""
         await self.start_server(do_udp=True)
-        with patch(
+        with mock.patch(
             "pymodbus.transaction.ModbusSocketFramer.processIncomingPacket",
-            new_callable=Mock,
+            new_callable=mock.Mock,
         ) as process:
             self.server.endpoint.datagram_received(data=b"12345", addr=(SERV_IP, 12345))
             await asyncio.sleep(0.1)
             process.seal()
             process.assert_called_once()
-            self.assertTrue(process.call_args[1]["data"] == b"12345")
+            assert process.call_args[1]["data"] == b"12345"
 
     async def test_async_udp_server_send_data(self):
         """Test that the modbus udp asyncio server correctly sends data outbound"""
@@ -384,7 +365,7 @@ async def test_async_udp_server_send_data(self):
         random_port = self.server.protocol._sock.getsockname()[  # pylint: disable=protected-access
             1
         ]
-        received = self.server.endpoint.datagram_received = Mock(
+        received = self.server.endpoint.datagram_received = mock.Mock(
             wraps=self.server.endpoint.datagram_received
         )
         await self.loop.create_datagram_endpoint(
@@ -392,15 +373,15 @@ async def test_async_udp_server_send_data(self):
         )
         await asyncio.sleep(0.1)
         received.assert_called_once()
-        self.assertEqual(received.call_args[0][0], BasicClient.dataTo)
+        assert received.call_args[0][0] == BasicClient.dataTo
         await self.server.server_close()
         self.server = None
 
     async def test_async_udp_server_roundtrip(self):
         """Test sending and receiving data on udp socket"""
         expected_response = b"\x01\x00\x00\x00\x00\x05\x01\x03\x02\x00\x11"  # value of 17 as per context
-        BasicClient.dataTo = TEST_DATA  # unit 1, read register
-        BasicClient.done = self.loop.create_future()
+        BasicClient.dataTo = TEST_DATA  # slave 1, read register
+        BasicClient.done = asyncio.Future()
         await self.start_server(do_udp=True)
         random_port = self.server.protocol._sock.getsockname()[  # pylint: disable=protected-access
             1
@@ -409,18 +390,18 @@ async def test_async_udp_server_roundtrip(self):
             BasicClient, remote_addr=("127.0.0.1", random_port)
         )
         await asyncio.wait_for(BasicClient.done, timeout=0.1)
-        self.assertEqual(BasicClient.received_data, expected_response)
+        assert BasicClient.received_data == expected_response
         transport.close()
 
     async def test_async_udp_server_exception(self):
         """Test sending garbage data on a TCP socket should drop the connection"""
         BasicClient.dataTo = b"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF"
-        BasicClient.connected = self.loop.create_future()
-        BasicClient.done = self.loop.create_future()
+        BasicClient.connected = asyncio.Future()
+        BasicClient.done = asyncio.Future()
         await self.start_server(do_udp=True)
-        with patch(
+        with mock.patch(
             "pymodbus.transaction.ModbusSocketFramer.processIncomingPacket",
-            new_callable=lambda: Mock(side_effect=Exception),
+            new_callable=lambda: mock.Mock(side_effect=Exception),
         ):
             # get the random server port pylint: disable=protected-access
             random_port = self.server.protocol._sock.getsockname()[1]
@@ -428,18 +409,16 @@ async def test_async_udp_server_exception(self):
                 BasicClient, remote_addr=("127.0.0.1", random_port)
             )
             await asyncio.wait_for(BasicClient.connected, timeout=0.1)
-            self.assertFalse(BasicClient.done.done())
-            self.assertFalse(
-                self.server.protocol._sock._closed  # pylint: disable=protected-access
-            )
+            assert not BasicClient.done.done()
+            assert not self.server.protocol._sock._closed
 
     async def test_async_tcp_server_exception(self):
         """Send garbage data on a TCP socket should drop the connection"""
         BasicClient.data = b"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF"
         await self.start_server()
-        with patch(
+        with mock.patch(
             "pymodbus.transaction.ModbusSocketFramer.processIncomingPacket",
-            new_callable=lambda: Mock(side_effect=Exception),
+            new_callable=lambda: mock.Mock(side_effect=Exception),
         ):
             await self.connect_server()
             await asyncio.wait_for(BasicClient.eof, timeout=0.1)
diff --git a/test/test_server_context.py b/test/test_server_context.py
index d4c48736c..9632ae33b 100644
--- a/test/test_server_context.py
+++ b/test/test_server_context.py
@@ -1,26 +1,24 @@
 """Test server context."""
-import unittest
+import pytest
 
 from pymodbus.datastore import ModbusServerContext, ModbusSlaveContext
 from pymodbus.exceptions import NoSuchSlaveException
 
 
-class ModbusServerSingleContextTest(unittest.TestCase):
-    """This is the unittest for the pymodbus.datastore.ModbusServerContext using a single slave context."""
+class TestServerSingleContext:
+    """This is the test for the pymodbus.datastore.ModbusServerContext using a single slave context."""
 
-    def setUp(self):
+    slave = ModbusSlaveContext()
+    context = None
+
+    def setup_method(self):
         """Set up the test environment"""
-        self.slave = ModbusSlaveContext()
         self.context = ModbusServerContext(slaves=self.slave, single=True)
 
-    def tearDown(self):
-        """Clean up the test environment"""
-        del self.context
-
     def test_single_context_gets(self):
         """Test getting on a single context"""
         for slave_id in range(0, 0xFF):
-            self.assertEqual(self.slave, self.context[slave_id])
+            assert self.slave == self.context[slave_id]
 
     def test_single_context_deletes(self):
         """Test removing on multiple context"""
@@ -28,68 +26,70 @@ def test_single_context_deletes(self):
         def _test():
             del self.context[0x00]
 
-        self.assertRaises(NoSuchSlaveException, _test)
+        with pytest.raises(NoSuchSlaveException):
+            _test()
 
     def test_single_context_iter(self):
         """Test iterating over a single context"""
         expected = (0, self.slave)
         for slave in self.context:
-            self.assertEqual(slave, expected)
+            assert slave == expected
 
     def test_single_context_default(self):
         """Test that the single context default values work"""
         self.context = ModbusServerContext()
         slave = self.context[0x00]
-        self.assertEqual(slave, {})
+        assert not slave
 
     def test_single_context_set(self):
         """Test a setting a single slave context"""
         slave = ModbusSlaveContext()
         self.context[0x00] = slave
         actual = self.context[0x00]
-        self.assertEqual(slave, actual)
+        assert slave == actual
 
     def test_single_context_register(self):
         """Test single context register."""
         request_db = [1, 2, 3]
         slave = ModbusSlaveContext()
         slave.register(0xFF, "custom_request", request_db)
-        self.assertEqual(slave.store["custom_request"], request_db)
-        self.assertEqual(slave.decode(0xFF), "custom_request")
+        assert slave.store["custom_request"] == request_db
+        assert slave.decode(0xFF) == "custom_request"
 
 
-class ModbusServerMultipleContextTest(unittest.TestCase):
-    """This is the unittest for the pymodbus.datastore.ModbusServerContext using multiple slave contexts."""
+class TestServerMultipleContext:
+    """This is the test for the pymodbus.datastore.ModbusServerContext using multiple slave contexts."""
 
-    def setUp(self):
+    slaves = None
+    context = None
+
+    def setup_method(self):
         """Set up the test environment"""
         self.slaves = {id: ModbusSlaveContext() for id in range(10)}
         self.context = ModbusServerContext(slaves=self.slaves, single=False)
 
-    def tearDown(self):
-        """Clean up the test environment"""
-        del self.context
-
     def test_multiple_context_gets(self):
         """Test getting on multiple context"""
         for slave_id in range(0, 10):
-            self.assertEqual(self.slaves[slave_id], self.context[slave_id])
+            assert self.slaves[slave_id] == self.context[slave_id]
 
     def test_multiple_context_deletes(self):
         """Test removing on multiple context"""
         del self.context[0x00]
-        self.assertRaises(NoSuchSlaveException, lambda: self.context[0x00])
+        with pytest.raises(NoSuchSlaveException):
+            self.context[0x00]()
 
     def test_multiple_context_iter(self):
         """Test iterating over multiple context"""
         for slave_id, slave in self.context:
-            self.assertEqual(slave, self.slaves[slave_id])
-            self.assertTrue(slave_id in self.context)
+            assert slave == self.slaves[slave_id]
+            assert slave_id in self.context
 
     def test_multiple_context_default(self):
         """Test that the multiple context default values work"""
         self.context = ModbusServerContext(single=False)
-        self.assertRaises(NoSuchSlaveException, lambda: self.context[0x00])
+        with pytest.raises(NoSuchSlaveException):
+            self.context[0x00]()
 
     def test_multiple_context_set(self):
         """Test a setting multiple slave contexts"""
@@ -98,4 +98,4 @@ def test_multiple_context_set(self):
             self.context[slave_id] = slave
         for slave_id, slave in iter(slaves.items()):
             actual = self.context[slave_id]
-            self.assertEqual(slave, actual)
+            assert slave == actual
diff --git a/test/test_server_multidrop.py b/test/test_server_multidrop.py
new file mode 100644
index 000000000..a13b76747
--- /dev/null
+++ b/test/test_server_multidrop.py
@@ -0,0 +1,169 @@
+"""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."""
+        framer_ok = b"\x02\x03\x00\x01\x00}\xd4\x18"
+        framer._buffer = framer_ok  # pylint: disable=protected-access
+        assert framer.getFrameStart(self.slaves, False, False)
+        assert framer_ok == framer._buffer  # pylint: disable=protected-access
+
+        framer_2ok = framer_ok + framer_ok
+        framer._buffer = framer_2ok  # pylint: disable=protected-access
+        assert framer.getFrameStart(self.slaves, False, False)
+        assert framer_2ok == framer._buffer  # pylint: disable=protected-access
+        assert framer.getFrameStart(self.slaves, False, True)
+        assert framer_ok == framer._buffer  # pylint: disable=protected-access
+
+        framer._buffer = framer_ok[:2]  # pylint: disable=protected-access
+        assert not framer.getFrameStart(self.slaves, False, False)
+        assert framer_ok[:2] == framer._buffer  # pylint: disable=protected-access
+
+        framer._buffer = framer_ok[:3]  # pylint: disable=protected-access
+        assert not framer.getFrameStart(self.slaves, False, False)
+        assert framer_ok[:3] == framer._buffer  # pylint: disable=protected-access
+
+        framer_ok = b"\xF0\x03\x00\x01\x00}\xd4\x18"
+        framer._buffer = framer_ok  # pylint: disable=protected-access
+        assert not framer.getFrameStart(self.slaves, False, False)
+        assert framer._buffer == framer_ok[-3:]  # pylint: disable=protected-access
diff --git a/test/test_server_task.py b/test/test_server_task.py
index d1c7bc582..df382c65d 100755
--- a/test/test_server_task.py
+++ b/test/test_server_task.py
@@ -4,6 +4,7 @@
 import os
 from threading import Thread
 from time import sleep
+from unittest import mock
 
 import pytest
 
@@ -35,7 +36,7 @@ def helper_config(request, def_type):
     datablock = ModbusSequentialDataBlock(0x00, [17] * 100)
     context = ModbusServerContext(
         slaves=ModbusSlaveContext(
-            di=datablock, co=datablock, hr=datablock, ir=datablock, unit=1
+            di=datablock, co=datablock, hr=datablock, ir=datablock, slave=1
         ),
         single=True,
     )
@@ -139,23 +140,23 @@ def helper_config(request, def_type):
     return cur_m["srv"], cur["srv_args"], cur_m["cli"], cur["cli_args"]
 
 
-@pytest.mark.xdist_group(name="task_serialize")
+@pytest.mark.xdist_group(name="server_serialize")
 @pytest.mark.parametrize("comm", TEST_TYPES)
 async def test_async_task_no_server(comm):
     """Test normal client/server handling."""
-    run_server, server_args, run_client, client_args = helper_config(comm, "async")
+    _run_server, _server_args, run_client, client_args = helper_config(comm, "async")
     client = run_client(**client_args)
     try:
         await client.connect()
-    except Exception as exc:  # pylint: disable=broad-except
-        assert False, f"unexpected exception: {exc}"
+    except Exception as exc:
+        raise AssertionError(f"unexpected exception: {exc}") from exc
     await asyncio.sleep(0.1)
     with pytest.raises((asyncio.exceptions.TimeoutError, ConnectionException)):
         await client.read_coils(1, 1, slave=0x01)
-    await client.close()
+    client.close()
 
 
-@pytest.mark.xdist_group(name="task_serialize")
+@pytest.mark.xdist_group(name="server_serialize")
 @pytest.mark.parametrize("comm", TEST_TYPES)
 async def test_async_task_ok(comm):
     """Test normal client/server handling."""
@@ -166,29 +167,68 @@ async def test_async_task_ok(comm):
     client = run_client(**client_args)
     await client.connect()
     await asyncio.sleep(0.1)
-    assert client._connected  # pylint: disable=protected-access
+    assert client.transport
     rr = await client.read_coils(1, 1, slave=0x01)
     assert len(rr.bits) == 8
 
-    await client.close()
+    client.close()
     await asyncio.sleep(0.1)
-    assert not client._connected  # pylint: disable=protected-access
+    assert not client.transport
     await server.ServerAsyncStop()
+    task.cancel()
     await task
 
 
-@pytest.mark.xdist_group(name="task_serialize")
+@pytest.mark.xdist_group(name="server_serialize")
 @pytest.mark.parametrize("comm", TEST_TYPES)
-async def test_async_task_server_stop(comm):
+async def test_async_task_reuse(comm):
     """Test normal client/server handling."""
     run_server, server_args, run_client, client_args = helper_config(comm, "async")
+
     task = asyncio.create_task(run_server(**server_args))
     await asyncio.sleep(0.1)
     client = run_client(**client_args)
     await client.connect()
-    assert client._connected  # pylint: disable=protected-access
+    await asyncio.sleep(0.1)
+    assert client.transport
+    rr = await client.read_coils(1, 1, slave=0x01)
+    assert len(rr.bits) == 8
+
+    client.close()
+    await asyncio.sleep(0.1)
+    assert not client.transport
+
+    await client.connect()
+    await asyncio.sleep(0.1)
+    assert client.transport
+    rr = await client.read_coils(1, 1, slave=0x01)
+    assert len(rr.bits) == 8
+
+    client.close()
+    await asyncio.sleep(0.1)
+    assert not client.transport
+
+    await server.ServerAsyncStop()
+    task.cancel()
+    await task
+
+
+@pytest.mark.xdist_group(name="server_serialize")
+@pytest.mark.parametrize("comm", TEST_TYPES)
+async def test_async_task_server_stop(comm):
+    """Test normal client/server handling."""
+    run_server, server_args, run_client, client_args = helper_config(comm, "async")
+    task = asyncio.create_task(run_server(**server_args))
+    await asyncio.sleep(0.5)
+
+    on_reconnect_callback = mock.Mock()
+
+    client = run_client(**client_args, on_reconnect_callback=on_reconnect_callback)
+    await client.connect()
+    assert client.transport
     rr = await client.read_coils(1, 1, slave=0x01)
     assert len(rr.bits) == 8
+    on_reconnect_callback.assert_not_called()
 
     # Server breakdown
     await server.ServerAsyncStop()
@@ -196,31 +236,30 @@ async def test_async_task_server_stop(comm):
 
     with pytest.raises((ConnectionException, asyncio.exceptions.TimeoutError)):
         rr = await client.read_coils(1, 1, slave=0x01)
-    assert not client._connected  # pylint: disable=protected-access
+    assert not client.transport
 
     # Server back online
     task = asyncio.create_task(run_server(**server_args))
-    await asyncio.sleep(0.1)
+    await asyncio.sleep(1)
 
     timer_allowed = 100
-    while not client._connected:  # pylint: disable=protected-access
+    while not client.transport and timer_allowed:
         await asyncio.sleep(0.1)
         timer_allowed -= 1
-        if not timer_allowed:
-            assert False, "client do not reconnect"
-    assert client._connected  # pylint: disable=protected-access
+    assert client.transport, "client do not reconnect"
+    # TBD on_reconnect_callback.assert_called()
 
     rr = await client.read_coils(1, 1, slave=0x01)
     assert len(rr.bits) == 8
 
-    await client.close()
+    client.close()
     await asyncio.sleep(0.5)
-    assert not client._connected  # pylint: disable=protected-access
+    assert not client.transport
     await server.ServerAsyncStop()
     await task
 
 
-@pytest.mark.xdist_group(name="task_serialize")
+@pytest.mark.xdist_group(name="server_serialize")
 @pytest.mark.parametrize("comm", TEST_TYPES)
 def test_sync_task_no_server(comm):
     """Test normal client/server handling."""
@@ -228,8 +267,8 @@ def test_sync_task_no_server(comm):
     client = run_client(**client_args)
     try:
         client.connect()
-    except Exception as exc:  # pylint: disable=broad-except
-        assert False, f"unexpected exception: {exc}"
+    except Exception as exc:
+        raise AssertionError(f"unexpected exception: {exc}") from exc
     sleep(0.1)
     if comm == "udp":
         rr = client.read_coils(1, 1, slave=0x01)
@@ -240,7 +279,7 @@ def test_sync_task_no_server(comm):
     client.close()
 
 
-@pytest.mark.xdist_group(name="task_serialize")
+@pytest.mark.xdist_group(name="server_serialize")
 @pytest.mark.parametrize("comm", TEST_TYPES)
 def test_sync_task_ok(comm):
     """Test normal client/server handling."""
@@ -265,7 +304,7 @@ def test_sync_task_ok(comm):
     thread.join()
 
 
-@pytest.mark.xdist_group(name="task_serialize")
+@pytest.mark.xdist_group(name="server_serialize")
 @pytest.mark.parametrize("comm", TEST_TYPES)
 def test_sync_task_server_stop(comm):
     """Test normal client/server handling."""
@@ -304,7 +343,7 @@ def test_sync_task_server_stop(comm):
         sleep(0.1)
         timer_allowed -= 1
         if not timer_allowed:
-            assert False, "client do not reconnect"
+            pytest.fail("client do not reconnect")
     assert client.socket
 
     rr = client.read_coils(1, 1, slave=0x01)
diff --git a/test/test_simulator.py b/test/test_simulator.py
index 036be5a40..ead7bfc9e 100644
--- a/test/test_simulator.py
+++ b/test/test_simulator.py
@@ -1,18 +1,10 @@
 """Test datastore."""
-import asyncio
 import copy
-import logging
 
 import pytest
 
-from examples.client_async import setup_async_client
-from examples.helper import Commandline
-from examples.server_simulator import run_server_simulator, setup_simulator
-from pymodbus import pymodbus_apply_logging_config
 from pymodbus.datastore import ModbusSimulatorContext
 from pymodbus.datastore.simulator import Cell, CellType, Label
-from pymodbus.server import ServerAsyncStop
-from pymodbus.transaction import ModbusSocketFramer
 
 
 FX_READ_BIT = 1
@@ -412,7 +404,7 @@ def test_simulator_get_text(self):
             assert cell.count_write == str(reg.count_write), f"at register {test_reg}"
 
     @pytest.mark.parametrize(
-        "func,addr",
+        ("func", "addr"),
         [
             (FX_READ_BIT, 12),
             (FX_READ_REG, 16),
@@ -462,27 +454,3 @@ def test_simulator_action_reset(self):
         ]
         with pytest.raises(RuntimeError):
             exc_simulator.getValues(FX_READ_REG, addr, 1)
-
-    async def test_simulator_example(self):
-        """Test datastore simulator example."""
-        pymodbus_apply_logging_config(logging.DEBUG)
-        # JAN activate.
-        args = Commandline.copy()
-        args.comm = "tcp"
-        args.framer = ModbusSocketFramer
-        args.port = 5051
-        run_args = setup_simulator(
-            args, setup=self.default_config, actions=self.custom_actions
-        )
-        if args:
-            return  # Turn off for now.
-        asyncio.create_task(run_server_simulator(run_args))
-        await asyncio.sleep(0.1)
-        client = setup_async_client(args)
-        await client.connect()
-        assert client.connected
-
-        rr = await client.read_holding_registers(16, 1, slave=1)
-        assert rr.registers
-        await client.close()
-        await ServerAsyncStop()
diff --git a/test/test_sparse_datastore.py b/test/test_sparse_datastore.py
new file mode 100644
index 000000000..409fc3e01
--- /dev/null
+++ b/test/test_sparse_datastore.py
@@ -0,0 +1,25 @@
+"""Test framers."""
+
+
+from pymodbus.datastore import ModbusSparseDataBlock
+
+
+def test_check_sparsedatastore():
+    """Test check frame."""
+    data_in_block = {
+        1: 6720,
+        2: 130,
+        30: [0x0D, 0xFE],
+        105: [1, 2, 3, 4],
+        20000: [45, 241, 48],
+        20008: 38,
+        48140: [0x4208, 0xCCCD],
+    }
+    datablock = ModbusSparseDataBlock(data_in_block)
+    for key, entry in data_in_block.items():
+        if isinstance(entry, int):
+            entry = [entry]
+        for value in entry:
+            assert datablock.validate(key, 1)
+            assert datablock.getValues(key, 1) == [value]
+            key += 1
diff --git a/test/test_transaction.py b/test/test_transaction.py
index 87cacb5b5..967f6e443 100755
--- a/test/test_transaction.py
+++ b/test/test_transaction.py
@@ -1,8 +1,9 @@
 """Test transaction."""
-import unittest
 from binascii import a2b_hex
 from itertools import count
-from unittest.mock import MagicMock, patch
+from unittest import mock
+
+import pytest
 
 from pymodbus.exceptions import (
     InvalidMessageReceivedException,
@@ -25,15 +26,24 @@
 TEST_MESSAGE = b"\x7b\x01\x03\x00\x00\x00\x05\x85\xC9\x7d"
 
 
-class ModbusTransactionTest(  # pylint: disable=too-many-public-methods
-    unittest.TestCase
-):
+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
+    _binary = None
+    _manager = None
+    _queue_manager = None
+    _tm = None
+
     # ----------------------------------------------------------------------- #
     # Test Construction
     # ----------------------------------------------------------------------- #
-    def setUp(self):
+    def setup_method(self):
         """Set up the test environment"""
         self.client = None
         self.decoder = ServerDecoder()
@@ -46,31 +56,24 @@ def setUp(self):
         self._queue_manager = FifoTransactionManager(self.client)
         self._tm = ModbusTransactionManager(self.client)
 
-    def tearDown(self):
-        """Clean up the test environment"""
-        del self._manager
-        del self._tcp
-        del self._tls
-        del self._rtu
-        del self._ascii
-
     # ----------------------------------------------------------------------- #
     # Base transaction manager
     # ----------------------------------------------------------------------- #
 
     def test_calculate_expected_response_length(self):
         """Test calculate expected response length."""
-        self._tm.client = MagicMock()
-        self._tm.client.framer = MagicMock()
+        self._tm.client = mock.MagicMock()
+        self._tm.client.framer = mock.MagicMock()
         self._tm._set_adu_size()  # pylint: disable=protected-access
-        self.assertEqual(
-            self._tm._calculate_response_length(0),  # pylint: disable=protected-access
-            None,
+        assert (
+            not self._tm._calculate_response_length(  # pylint: disable=protected-access
+                0
+            )
         )
         self._tm.base_adu_size = 10
-        self.assertEqual(
-            self._tm._calculate_response_length(5),  # pylint: disable=protected-access
-            15,
+        assert (
+            self._tm._calculate_response_length(5)  # pylint: disable=protected-access
+            == 15
         )
 
     def test_calculate_exception_length(self):
@@ -83,7 +86,7 @@ def test_calculate_exception_length(self):
             ("tls", 2),
             ("dummy", None),
         ):
-            self._tm.client = MagicMock()
+            self._tm.client = mock.MagicMock()
             if framer == "ascii":
                 self._tm.client.framer = self._ascii
             elif framer == "binary":
@@ -95,97 +98,99 @@ def test_calculate_exception_length(self):
             elif framer == "tls":
                 self._tm.client.framer = self._tls
             else:
-                self._tm.client.framer = MagicMock()
+                self._tm.client.framer = mock.MagicMock()
 
             self._tm._set_adu_size()  # pylint: disable=protected-access
-            self.assertEqual(
-                self._tm._calculate_exception_length(),  # pylint: disable=protected-access
-                exception_length,
+            assert (
+                self._tm._calculate_exception_length()  # pylint: disable=protected-access
+                == exception_length
             )
 
-    @patch("pymodbus.transaction.time")
+    @mock.patch("pymodbus.transaction.time")
     def test_execute(self, mock_time):
         """Test execute."""
         mock_time.time.side_effect = count()
 
-        client = MagicMock()
+        client = mock.MagicMock()
         client.framer = self._ascii
         client.framer._buffer = b"deadbeef"  # pylint: disable=protected-access
-        client.framer.processIncomingPacket = MagicMock()
+        client.framer.processIncomingPacket = mock.MagicMock()
         client.framer.processIncomingPacket.return_value = None
-        client.framer.buildPacket = MagicMock()
+        client.framer.buildPacket = mock.MagicMock()
         client.framer.buildPacket.return_value = b"deadbeef"
-        client.framer.sendPacket = MagicMock()
+        client.framer.sendPacket = mock.MagicMock()
         client.framer.sendPacket.return_value = len(b"deadbeef")
-        client.framer.decode_data = MagicMock()
-        client.framer.decode_data.return_value = {"unit": 1, "fcode": 222, "length": 27}
-        request = MagicMock()
+        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.unit_id = 1
+        request.slave_id = 1
         request.function_code = 222
         trans = ModbusTransactionManager(client)
-        trans._recv = MagicMock(  # pylint: disable=protected-access
+        trans._recv = mock.MagicMock(  # pylint: disable=protected-access
             return_value=b"abcdef"
         )
-        self.assertEqual(trans.retries, 3)
-        self.assertEqual(trans.retry_on_empty, False)
+        assert trans.retries == 3
+        assert not trans.retry_on_empty
 
-        trans.getTransaction = MagicMock()
+        trans.getTransaction = mock.MagicMock()
         trans.getTransaction.return_value = "response"
         response = trans.execute(request)
-        self.assertEqual(response, "response")
+        assert response == "response"
         # No response
-        trans._recv = MagicMock(  # pylint: disable=protected-access
+        trans._recv = mock.MagicMock(  # pylint: disable=protected-access
             return_value=b"abcdef"
         )
         trans.transactions = []
-        trans.getTransaction = MagicMock()
+        trans.getTransaction = mock.MagicMock()
         trans.getTransaction.return_value = None
         response = trans.execute(request)
-        self.assertIsInstance(response, ModbusIOException)
+        assert isinstance(response, ModbusIOException)
 
         # No response with retries
         trans.retry_on_empty = True
-        trans._recv = MagicMock(  # pylint: disable=protected-access
+        trans._recv = mock.MagicMock(  # pylint: disable=protected-access
             side_effect=iter([b"", b"abcdef"])
         )
         response = trans.execute(request)
-        self.assertIsInstance(response, ModbusIOException)
+        assert isinstance(response, ModbusIOException)
 
         # wrong handle_local_echo
-        trans._recv = MagicMock(  # pylint: disable=protected-access
+        trans._recv = mock.MagicMock(  # pylint: disable=protected-access
             side_effect=iter([b"abcdef", b"deadbe", b"123456"])
         )
         client.handle_local_echo = True
         trans.retry_on_empty = False
         trans.retry_on_invalid = False
-        self.assertEqual(
-            trans.execute(request).message, "[Input/Output] Wrong local echo"
-        )
+        assert trans.execute(request).message == "[Input/Output] Wrong local echo"
         client.handle_local_echo = False
 
         # retry on invalid response
         trans.retry_on_invalid = True
-        trans._recv = MagicMock(  # pylint: disable=protected-access
+        trans._recv = mock.MagicMock(  # pylint: disable=protected-access
             side_effect=iter([b"", b"abcdef", b"deadbe", b"123456"])
         )
         response = trans.execute(request)
-        self.assertIsInstance(response, ModbusIOException)
+        assert isinstance(response, ModbusIOException)
 
         # Unable to decode response
-        trans._recv = MagicMock(  # pylint: disable=protected-access
+        trans._recv = mock.MagicMock(  # pylint: disable=protected-access
             side_effect=ModbusIOException()
         )
-        client.framer.processIncomingPacket.side_effect = MagicMock(
+        client.framer.processIncomingPacket.side_effect = mock.MagicMock(
             side_effect=ModbusIOException()
         )
-        self.assertIsInstance(trans.execute(request), ModbusIOException)
+        assert isinstance(trans.execute(request), ModbusIOException)
 
         # Broadcast
         client.params.broadcast_enable = True
-        request.unit_id = 0
+        request.slave_id = 0
         response = trans.execute(request)
-        self.assertEqual(response, b"Broadcast write sent - no response expected")
+        assert response == b"Broadcast write sent - no response expected"
 
     # ----------------------------------------------------------------------- #
     # Dictionary based transaction manager
@@ -194,9 +199,9 @@ def test_execute(self, mock_time):
     def test_dict_transaction_manager_tid(self):
         """Test the dict transaction manager TID"""
         for tid in range(1, self._manager.getNextTID() + 10):
-            self.assertEqual(tid + 1, self._manager.getNextTID())
+            assert tid + 1 == self._manager.getNextTID()
         self._manager.reset()
-        self.assertEqual(1, self._manager.getNextTID())
+        assert self._manager.getNextTID() == 1
 
     def test_get_dict_fifo_transaction_manager_transaction(self):
         """Test the dict transaction manager"""
@@ -212,7 +217,7 @@ class Request:  # pylint: disable=too-few-public-methods
         handle.message = b"testing"  # pylint: disable=attribute-defined-outside-init
         self._manager.addTransaction(handle)
         result = self._manager.getTransaction(handle.transaction_id)
-        self.assertEqual(handle.message, result.message)
+        assert handle.message == result.message
 
     def test_delete_dict_fifo_transaction_manager_transaction(self):
         """Test the dict transaction manager"""
@@ -229,7 +234,7 @@ class Request:  # pylint: disable=too-few-public-methods
 
         self._manager.addTransaction(handle)
         self._manager.delTransaction(handle.transaction_id)
-        self.assertEqual(None, self._manager.getTransaction(handle.transaction_id))
+        assert not self._manager.getTransaction(handle.transaction_id)
 
     # ----------------------------------------------------------------------- #
     # Queue based transaction manager
@@ -237,9 +242,9 @@ class Request:  # pylint: disable=too-few-public-methods
     def test_fifo_transaction_manager_tid(self):
         """Test the fifo transaction manager TID"""
         for tid in range(1, self._queue_manager.getNextTID() + 10):
-            self.assertEqual(tid + 1, self._queue_manager.getNextTID())
+            assert tid + 1 == self._queue_manager.getNextTID()
         self._queue_manager.reset()
-        self.assertEqual(1, self._queue_manager.getNextTID())
+        assert self._queue_manager.getNextTID() == 1
 
     def test_get_fifo_transaction_manager_transaction(self):
         """Test the fifo transaction manager"""
@@ -255,7 +260,7 @@ class Request:  # pylint: disable=too-few-public-methods
         handle.message = b"testing"  # pylint: disable=attribute-defined-outside-init
         self._queue_manager.addTransaction(handle)
         result = self._queue_manager.getTransaction(handle.transaction_id)
-        self.assertEqual(handle.message, result.message)
+        assert handle.message == result.message
 
     def test_delete_fifo_transaction_manager_transaction(self):
         """Test the fifo transaction manager"""
@@ -272,9 +277,7 @@ class Request:  # pylint: disable=too-few-public-methods
 
         self._queue_manager.addTransaction(handle)
         self._queue_manager.delTransaction(handle.transaction_id)
-        self.assertEqual(
-            None, self._queue_manager.getTransaction(handle.transaction_id)
-        )
+        assert not self._queue_manager.getTransaction(handle.transaction_id)
 
     # ----------------------------------------------------------------------- #
     # TCP tests
@@ -282,23 +285,23 @@ class Request:  # pylint: disable=too-few-public-methods
     def test_tcp_framer_transaction_ready(self):
         """Test a tcp frame transaction"""
         msg = b"\x00\x01\x12\x34\x00\x04\xff\x02\x12\x34"
-        self.assertFalse(self._tcp.isFrameReady())
-        self.assertFalse(self._tcp.checkFrame())
+        assert not self._tcp.isFrameReady()
+        assert not self._tcp.checkFrame()
         self._tcp.addToFrame(msg)
-        self.assertTrue(self._tcp.isFrameReady())
-        self.assertTrue(self._tcp.checkFrame())
+        assert self._tcp.isFrameReady()
+        assert self._tcp.checkFrame()
         self._tcp.advanceFrame()
-        self.assertFalse(self._tcp.isFrameReady())
-        self.assertFalse(self._tcp.checkFrame())
-        self.assertEqual(b"", self._ascii.getFrame())
+        assert not self._tcp.isFrameReady()
+        assert not self._tcp.checkFrame()
+        assert self._ascii.getFrame() == b""
 
     def test_tcp_framer_transaction_full(self):
         """Test a full tcp frame transaction"""
         msg = b"\x00\x01\x12\x34\x00\x04\xff\x02\x12\x34"
         self._tcp.addToFrame(msg)
-        self.assertTrue(self._tcp.checkFrame())
+        assert self._tcp.checkFrame()
         result = self._tcp.getFrame()
-        self.assertEqual(msg[7:], result)
+        assert result == msg[7:]
         self._tcp.advanceFrame()
 
     def test_tcp_framer_transaction_half(self):
@@ -306,13 +309,13 @@ def test_tcp_framer_transaction_half(self):
         msg1 = b"\x00\x01\x12\x34\x00"
         msg2 = b"\x04\xff\x02\x12\x34"
         self._tcp.addToFrame(msg1)
-        self.assertFalse(self._tcp.checkFrame())
+        assert not self._tcp.checkFrame()
         result = self._tcp.getFrame()
-        self.assertEqual(b"", result)
+        assert result == b""
         self._tcp.addToFrame(msg2)
-        self.assertTrue(self._tcp.checkFrame())
+        assert self._tcp.checkFrame()
         result = self._tcp.getFrame()
-        self.assertEqual(msg2[2:], result)
+        assert result == msg2[2:]
         self._tcp.advanceFrame()
 
     def test_tcp_framer_transaction_half2(self):
@@ -320,13 +323,13 @@ def test_tcp_framer_transaction_half2(self):
         msg1 = b"\x00\x01\x12\x34\x00\x04\xff"
         msg2 = b"\x02\x12\x34"
         self._tcp.addToFrame(msg1)
-        self.assertFalse(self._tcp.checkFrame())
+        assert not self._tcp.checkFrame()
         result = self._tcp.getFrame()
-        self.assertEqual(b"", result)
+        assert result == b""
         self._tcp.addToFrame(msg2)
-        self.assertTrue(self._tcp.checkFrame())
+        assert self._tcp.checkFrame()
         result = self._tcp.getFrame()
-        self.assertEqual(msg2, result)
+        assert msg2 == result
         self._tcp.advanceFrame()
 
     def test_tcp_framer_transaction_half3(self):
@@ -334,13 +337,13 @@ def test_tcp_framer_transaction_half3(self):
         msg1 = b"\x00\x01\x12\x34\x00\x04\xff\x02\x12"
         msg2 = b"\x34"
         self._tcp.addToFrame(msg1)
-        self.assertFalse(self._tcp.checkFrame())
+        assert not self._tcp.checkFrame()
         result = self._tcp.getFrame()
-        self.assertEqual(msg1[7:], result)
+        assert result == msg1[7:]
         self._tcp.addToFrame(msg2)
-        self.assertTrue(self._tcp.checkFrame())
+        assert self._tcp.checkFrame()
         result = self._tcp.getFrame()
-        self.assertEqual(msg1[7:] + msg2, result)
+        assert result == msg1[7:] + msg2
         self._tcp.advanceFrame()
 
     def test_tcp_framer_transaction_short(self):
@@ -348,15 +351,15 @@ def test_tcp_framer_transaction_short(self):
         msg1 = b"\x99\x99\x99\x99\x00\x01\x00\x01"
         msg2 = b"\x00\x01\x12\x34\x00\x04\xff\x02\x12\x34"
         self._tcp.addToFrame(msg1)
-        self.assertFalse(self._tcp.checkFrame())
+        assert not self._tcp.checkFrame()
         result = self._tcp.getFrame()
-        self.assertEqual(b"", result)
+        assert result == b""
         self._tcp.advanceFrame()
         self._tcp.addToFrame(msg2)
-        self.assertEqual(10, len(self._tcp._buffer))  # pylint: disable=protected-access
-        self.assertTrue(self._tcp.checkFrame())
+        assert len(self._tcp._buffer) == 10  # pylint: disable=protected-access
+        assert self._tcp.checkFrame()
         result = self._tcp.getFrame()
-        self.assertEqual(msg2[7:], result)
+        assert result == msg2[7:]
         self._tcp.advanceFrame()
 
     def test_tcp_framer_populate(self):
@@ -364,14 +367,14 @@ def test_tcp_framer_populate(self):
         expected = ModbusRequest()
         expected.transaction_id = 0x0001
         expected.protocol_id = 0x1234
-        expected.unit_id = 0xFF
+        expected.slave_id = 0xFF
         msg = b"\x00\x01\x12\x34\x00\x04\xff\x02\x12\x34"
         self._tcp.addToFrame(msg)
-        self.assertTrue(self._tcp.checkFrame())
+        assert self._tcp.checkFrame()
         actual = ModbusRequest()
         self._tcp.populateResult(actual)
-        for name in ("transaction_id", "protocol_id", "unit_id"):
-            self.assertEqual(getattr(expected, name), getattr(actual, name))
+        for name in ("transaction_id", "protocol_id", "slave_id"):
+            assert getattr(expected, name) == getattr(actual, name)
         self._tcp.advanceFrame()
 
     def test_tcp_framer_packet(self):
@@ -381,11 +384,11 @@ def test_tcp_framer_packet(self):
         message = ModbusRequest()
         message.transaction_id = 0x0001
         message.protocol_id = 0x1234
-        message.unit_id = 0xFF
+        message.slave_id = 0xFF
         message.function_code = 0x01
         expected = b"\x00\x01\x12\x34\x00\x02\xff\x01"
         actual = self._tcp.buildPacket(message)
-        self.assertEqual(expected, actual)
+        assert expected == actual
         ModbusRequest.encode = old_encode
 
     # ----------------------------------------------------------------------- #
@@ -394,23 +397,23 @@ def test_tcp_framer_packet(self):
     def test_framer_tls_framer_transaction_ready(self):
         """Test a tls frame transaction"""
         msg = b"\x01\x12\x34\x00\x08"
-        self.assertFalse(self._tls.isFrameReady())
-        self.assertFalse(self._tls.checkFrame())
+        assert not self._tls.isFrameReady()
+        assert not self._tls.checkFrame()
         self._tls.addToFrame(msg)
-        self.assertTrue(self._tls.isFrameReady())
-        self.assertTrue(self._tls.checkFrame())
+        assert self._tls.isFrameReady()
+        assert self._tls.checkFrame()
         self._tls.advanceFrame()
-        self.assertFalse(self._tls.isFrameReady())
-        self.assertFalse(self._tls.checkFrame())
-        self.assertEqual(b"", self._tls.getFrame())
+        assert not self._tls.isFrameReady()
+        assert not self._tls.checkFrame()
+        assert self._tls.getFrame() == b""
 
     def test_framer_tls_framer_transaction_full(self):
         """Test a full tls frame transaction"""
         msg = b"\x01\x12\x34\x00\x08"
         self._tls.addToFrame(msg)
-        self.assertTrue(self._tls.checkFrame())
+        assert self._tls.checkFrame()
         result = self._tls.getFrame()
-        self.assertEqual(msg[0:], result)
+        assert result == msg[0:]
         self._tls.advanceFrame()
 
     def test_framer_tls_framer_transaction_half(self):
@@ -418,13 +421,13 @@ def test_framer_tls_framer_transaction_half(self):
         msg1 = b""
         msg2 = b"\x01\x12\x34\x00\x08"
         self._tls.addToFrame(msg1)
-        self.assertFalse(self._tls.checkFrame())
+        assert not self._tls.checkFrame()
         result = self._tls.getFrame()
-        self.assertEqual(b"", result)
+        assert result == b""
         self._tls.addToFrame(msg2)
-        self.assertTrue(self._tls.checkFrame())
+        assert self._tls.checkFrame()
         result = self._tls.getFrame()
-        self.assertEqual(msg2[0:], result)
+        assert result == msg2[0:]
         self._tls.advanceFrame()
 
     def test_framer_tls_framer_transaction_short(self):
@@ -432,15 +435,15 @@ def test_framer_tls_framer_transaction_short(self):
         msg1 = b""
         msg2 = b"\x01\x12\x34\x00\x08"
         self._tls.addToFrame(msg1)
-        self.assertFalse(self._tls.checkFrame())
+        assert not self._tls.checkFrame()
         result = self._tls.getFrame()
-        self.assertEqual(b"", result)
+        assert result == b""
         self._tls.advanceFrame()
         self._tls.addToFrame(msg2)
-        self.assertEqual(5, len(self._tls._buffer))  # pylint: disable=protected-access
-        self.assertTrue(self._tls.checkFrame())
+        assert len(self._tls._buffer) == 5  # pylint: disable=protected-access
+        assert self._tls.checkFrame()
         result = self._tls.getFrame()
-        self.assertEqual(msg2[0:], result)
+        assert result == msg2[0:]
         self._tls.advanceFrame()
 
     def test_framer_tls_framer_decode(self):
@@ -448,39 +451,36 @@ def test_framer_tls_framer_decode(self):
         msg1 = b""
         msg2 = b"\x01\x12\x34\x00\x08"
         result = self._tls.decode_data(msg1)
-        self.assertEqual({}, result)
+        assert not result
         result = self._tls.decode_data(msg2)
-        self.assertEqual({"fcode": 1}, result)
+        assert result == {"fcode": 1}
         self._tls.advanceFrame()
 
     def test_framer_tls_incoming_packet(self):
         """Framer tls incoming packet."""
         msg = b"\x01\x12\x34\x00\x08"
 
-        unit = 0x01
+        slave = 0x01
 
         def mock_callback():
             """Mock callback."""
 
-        self._tls._process = MagicMock()  # pylint: disable=protected-access
-        self._tls.isFrameReady = MagicMock(return_value=False)
-        self._tls.processIncomingPacket(msg, mock_callback, unit)
-        self.assertEqual(msg, self._tls.getRawFrame())
+        self._tls._process = mock.MagicMock()  # pylint: disable=protected-access
+        self._tls.isFrameReady = mock.MagicMock(return_value=False)
+        self._tls.processIncomingPacket(msg, mock_callback, slave)
+        assert msg == self._tls.getRawFrame()
         self._tls.advanceFrame()
 
-        self._tls.isFrameReady = MagicMock(return_value=True)
-        self._tls._validate_unit_id = MagicMock(  # pylint: disable=protected-access
-            return_value=False
-        )
-        self._tls.processIncomingPacket(msg, mock_callback, unit)
-        self.assertEqual(b"", self._tls.getRawFrame())
+        self._tls.isFrameReady = mock.MagicMock(return_value=True)
+        x = mock.MagicMock(return_value=False)
+        self._tls._validate_slave_id = x  # pylint: disable=protected-access
+        self._tls.processIncomingPacket(msg, mock_callback, slave)
+        assert not self._tls.getRawFrame()
         self._tls.advanceFrame()
-
-        self._tls._validate_unit_id = MagicMock(  # pylint: disable=protected-access
-            return_value=True
-        )
-        self._tls.processIncomingPacket(msg, mock_callback, unit)
-        self.assertEqual(msg, self._tls.getRawFrame())
+        x = mock.MagicMock(return_value=True)
+        self._tls._validate_slave_id = x  # pylint: disable=protected-access
+        self._tls.processIncomingPacket(msg, mock_callback, slave)
+        assert msg == self._tls.getRawFrame()
         self._tls.advanceFrame()
 
     def test_framer_tls_process(self):
@@ -496,37 +496,30 @@ def __init__(self, code):
         def mock_callback(_arg):
             """Mock callback."""
 
-        self._tls.decoder.decode = MagicMock(return_value=None)
-        self.assertRaises(
-            ModbusIOException,
-            lambda: self._tls._process(  # pylint: disable=protected-access
-                mock_callback
-            ),
-        )
+        self._tls.decoder.decode = mock.MagicMock(return_value=None)
+        with pytest.raises(ModbusIOException):
+            self._tls._process(mock_callback)  # pylint: disable=protected-access
 
         result = MockResult(0x01)
-        self._tls.decoder.decode = MagicMock(return_value=result)
-        self.assertRaises(
-            InvalidMessageReceivedException,
-            lambda: self._tls._process(  # pylint: disable=protected-access
+        self._tls.decoder.decode = mock.MagicMock(return_value=result)
+        with pytest.raises(InvalidMessageReceivedException):
+            self._tls._process(  # pylint: disable=protected-access
                 mock_callback, error=True
-            ),
-        )
-
+            )
         self._tls._process(mock_callback)  # pylint: disable=protected-access
-        self.assertEqual(b"", self._tls.getRawFrame())
+        assert not self._tls.getRawFrame()
 
     def test_framer_tls_framer_populate(self):
         """Test a tls frame packet build"""
         ModbusRequest()
         msg = b"\x01\x12\x34\x00\x08"
         self._tls.addToFrame(msg)
-        self.assertTrue(self._tls.checkFrame())
+        assert self._tls.checkFrame()
         actual = ModbusRequest()
         result = self._tls.populateResult(  # pylint: disable=assignment-from-none
             actual
         )
-        self.assertEqual(None, result)
+        assert not result
         self._tls.advanceFrame()
 
     def test_framer_tls_framer_packet(self):
@@ -537,7 +530,7 @@ def test_framer_tls_framer_packet(self):
         message.function_code = 0x01
         expected = b"\x01"
         actual = self._tls.buildPacket(message)
-        self.assertEqual(expected, actual)
+        assert expected == actual
         ModbusRequest.encode = old_encode
 
     # ----------------------------------------------------------------------- #
@@ -545,25 +538,25 @@ def test_framer_tls_framer_packet(self):
     # ----------------------------------------------------------------------- #
     def test_rtu_framer_transaction_ready(self):
         """Test if the checks for a complete frame work"""
-        self.assertFalse(self._rtu.isFrameReady())
+        assert not self._rtu.isFrameReady()
 
         msg_parts = [b"\x00\x01\x00", b"\x00\x00\x01\xfc\x1b"]
         self._rtu.addToFrame(msg_parts[0])
-        self.assertFalse(self._rtu.isFrameReady())
-        self.assertFalse(self._rtu.checkFrame())
+        assert not self._rtu.isFrameReady()
+        assert not self._rtu.checkFrame()
 
         self._rtu.addToFrame(msg_parts[1])
-        self.assertTrue(self._rtu.isFrameReady())
-        self.assertTrue(self._rtu.checkFrame())
+        assert self._rtu.isFrameReady()
+        assert self._rtu.checkFrame()
 
     def test_rtu_framer_transaction_full(self):
         """Test a full rtu frame transaction"""
         msg = b"\x00\x01\x00\x00\x00\x01\xfc\x1b"
         stripped_msg = msg[1:-2]
         self._rtu.addToFrame(msg)
-        self.assertTrue(self._rtu.checkFrame())
+        assert self._rtu.checkFrame()
         result = self._rtu.getFrame()
-        self.assertEqual(stripped_msg, result)
+        assert stripped_msg == result
         self._rtu.advanceFrame()
 
     def test_rtu_framer_transaction_half(self):
@@ -571,12 +564,12 @@ def test_rtu_framer_transaction_half(self):
         msg_parts = [b"\x00\x01\x00", b"\x00\x00\x01\xfc\x1b"]
         stripped_msg = b"".join(msg_parts)[1:-2]
         self._rtu.addToFrame(msg_parts[0])
-        self.assertFalse(self._rtu.checkFrame())
+        assert not self._rtu.checkFrame()
         self._rtu.addToFrame(msg_parts[1])
-        self.assertTrue(self._rtu.isFrameReady())
-        self.assertTrue(self._rtu.checkFrame())
+        assert self._rtu.isFrameReady()
+        assert self._rtu.checkFrame()
         result = self._rtu.getFrame()
-        self.assertEqual(stripped_msg, result)
+        assert stripped_msg == result
         self._rtu.advanceFrame()
 
     def test_rtu_framer_populate(self):
@@ -588,22 +581,21 @@ def test_rtu_framer_populate(self):
         self._rtu.populateResult(request)
 
         header_dict = self._rtu._header  # pylint: disable=protected-access
-        self.assertEqual(len(msg), header_dict["len"])
-        self.assertEqual(int(msg[0]), header_dict["uid"])
-        self.assertEqual(msg[-2:], header_dict["crc"])
-
-        self.assertEqual(0x00, request.unit_id)
+        assert len(msg) == header_dict["len"]
+        assert int(msg[0]) == header_dict["uid"]
+        assert msg[-2:] == header_dict["crc"]
+        assert not request.slave_id
 
     def test_rtu_framer_packet(self):
         """Test a rtu frame packet build"""
         old_encode = ModbusRequest.encode
         ModbusRequest.encode = lambda self: b""
         message = ModbusRequest()
-        message.unit_id = 0xFF
+        message.slave_id = 0xFF
         message.function_code = 0x01
         expected = b"\xff\x01\x81\x80"  # only header + CRC - no data
         actual = self._rtu.buildPacket(message)
-        self.assertEqual(expected, actual)
+        assert expected == actual
         ModbusRequest.encode = old_encode
 
     def test_rtu_decode_exception(self):
@@ -611,7 +603,7 @@ def test_rtu_decode_exception(self):
         message = b"\x00\x90\x02\x9c\x01"
         self._rtu.addToFrame(message)
         result = self._rtu.checkFrame()
-        self.assertTrue(result)
+        assert result
 
     def test_process(self):
         """Test process."""
@@ -626,40 +618,36 @@ def mock_callback(_arg):
             """Mock callback."""
 
         mock_result = MockResult(code=0)
-        self._rtu.getRawFrame = self._rtu.getFrame = MagicMock()
-        self._rtu.decoder = MagicMock()
-        self._rtu.decoder.decode = MagicMock(return_value=mock_result)
-        self._rtu.populateResult = MagicMock()
-        self._rtu.advanceFrame = MagicMock()
+        self._rtu.getRawFrame = self._rtu.getFrame = mock.MagicMock()
+        self._rtu.decoder = mock.MagicMock()
+        self._rtu.decoder.decode = mock.MagicMock(return_value=mock_result)
+        self._rtu.populateResult = mock.MagicMock()
+        self._rtu.advanceFrame = mock.MagicMock()
 
         self._rtu._process(mock_callback)  # pylint: disable=protected-access
         self._rtu.populateResult.assert_called_with(mock_result)
         self._rtu.advanceFrame.assert_called_with()
-        self.assertTrue(self._rtu.advanceFrame.called)
+        assert self._rtu.advanceFrame.called
 
         # Check errors
-        self._rtu.decoder.decode = MagicMock(return_value=None)
-        self.assertRaises(
-            ModbusIOException,
-            lambda: self._rtu._process(  # pylint: disable=protected-access
-                mock_callback
-            ),
-        )
+        self._rtu.decoder.decode = mock.MagicMock(return_value=None)
+        with pytest.raises(ModbusIOException):
+            self._rtu._process(mock_callback)  # pylint: disable=protected-access
 
     def test_rtu_process_incoming_packets(self):
         """Test rtu process incoming packets."""
         mock_data = b"\x00\x01\x00\x00\x00\x01\xfc\x1b"
-        unit = 0x00
+        slave = 0x00
 
         def mock_callback():
             """Mock callback."""
 
-        self._rtu.addToFrame = MagicMock()
-        self._rtu._process = MagicMock()  # pylint: disable=protected-access
-        self._rtu.isFrameReady = MagicMock(return_value=False)
+        self._rtu.addToFrame = mock.MagicMock()
+        self._rtu._process = mock.MagicMock()  # pylint: disable=protected-access
+        self._rtu.isFrameReady = mock.MagicMock(return_value=False)
         self._rtu._buffer = mock_data  # pylint: disable=protected-access
 
-        self._rtu.processIncomingPacket(mock_data, mock_callback, unit)
+        self._rtu.processIncomingPacket(mock_data, mock_callback, slave)
 
     # ----------------------------------------------------------------------- #
     # ASCII tests
@@ -667,24 +655,24 @@ def mock_callback():
     def test_ascii_framer_transaction_ready(self):
         """Test a ascii frame transaction"""
         msg = b":F7031389000A60\r\n"
-        self.assertFalse(self._ascii.isFrameReady())
-        self.assertFalse(self._ascii.checkFrame())
+        assert not self._ascii.isFrameReady()
+        assert not self._ascii.checkFrame()
         self._ascii.addToFrame(msg)
-        self.assertTrue(self._ascii.isFrameReady())
-        self.assertTrue(self._ascii.checkFrame())
+        assert self._ascii.isFrameReady()
+        assert self._ascii.checkFrame()
         self._ascii.advanceFrame()
-        self.assertFalse(self._ascii.isFrameReady())
-        self.assertFalse(self._ascii.checkFrame())
-        self.assertEqual(b"", self._ascii.getFrame())
+        assert not self._ascii.isFrameReady()
+        assert not self._ascii.checkFrame()
+        assert not self._ascii.getFrame()
 
     def test_ascii_framer_transaction_full(self):
         """Test a full ascii frame transaction"""
         msg = b"sss:F7031389000A60\r\n"
         pack = a2b_hex(msg[6:-4])
         self._ascii.addToFrame(msg)
-        self.assertTrue(self._ascii.checkFrame())
+        assert self._ascii.checkFrame()
         result = self._ascii.getFrame()
-        self.assertEqual(pack, result)
+        assert pack == result
         self._ascii.advanceFrame()
 
     def test_ascii_framer_transaction_half(self):
@@ -693,46 +681,46 @@ def test_ascii_framer_transaction_half(self):
         msg2 = b"000A60\r\n"
         pack = a2b_hex(msg1[6:] + msg2[:-4])
         self._ascii.addToFrame(msg1)
-        self.assertFalse(self._ascii.checkFrame())
+        assert not self._ascii.checkFrame()
         result = self._ascii.getFrame()
-        self.assertEqual(b"", result)
+        assert not result
         self._ascii.addToFrame(msg2)
-        self.assertTrue(self._ascii.checkFrame())
+        assert self._ascii.checkFrame()
         result = self._ascii.getFrame()
-        self.assertEqual(pack, result)
+        assert pack == result
         self._ascii.advanceFrame()
 
     def test_ascii_framer_populate(self):
         """Test a ascii frame packet build"""
         request = ModbusRequest()
         self._ascii.populateResult(request)
-        self.assertEqual(0x00, request.unit_id)
+        assert not request.slave_id
 
     def test_ascii_framer_packet(self):
         """Test a ascii frame packet build"""
         old_encode = ModbusRequest.encode
         ModbusRequest.encode = lambda self: b""
         message = ModbusRequest()
-        message.unit_id = 0xFF
+        message.slave_id = 0xFF
         message.function_code = 0x01
         expected = b":FF0100\r\n"
         actual = self._ascii.buildPacket(message)
-        self.assertEqual(expected, actual)
+        assert expected == actual
         ModbusRequest.encode = old_encode
 
     def test_ascii_process_incoming_packets(self):
         """Test ascii process incoming packet."""
         mock_data = b":F7031389000A60\r\n"
-        unit = 0x00
+        slave = 0x00
 
         def mock_callback(_mock_data, *_args, **_kwargs):
             """Mock callback."""
 
-        self._ascii.processIncomingPacket(mock_data, mock_callback, unit)
+        self._ascii.processIncomingPacket(mock_data, mock_callback, slave)
 
         # Test failure:
-        self._ascii.checkFrame = MagicMock(return_value=False)
-        self._ascii.processIncomingPacket(mock_data, mock_callback, unit)
+        self._ascii.checkFrame = mock.MagicMock(return_value=False)
+        self._ascii.processIncomingPacket(mock_data, mock_callback, slave)
 
     # ----------------------------------------------------------------------- #
     # Binary tests
@@ -740,24 +728,24 @@ def mock_callback(_mock_data, *_args, **_kwargs):
     def test_binary_framer_transaction_ready(self):
         """Test a binary frame transaction"""
         msg = TEST_MESSAGE
-        self.assertFalse(self._binary.isFrameReady())
-        self.assertFalse(self._binary.checkFrame())
+        assert not self._binary.isFrameReady()
+        assert not self._binary.checkFrame()
         self._binary.addToFrame(msg)
-        self.assertTrue(self._binary.isFrameReady())
-        self.assertTrue(self._binary.checkFrame())
+        assert self._binary.isFrameReady()
+        assert self._binary.checkFrame()
         self._binary.advanceFrame()
-        self.assertFalse(self._binary.isFrameReady())
-        self.assertFalse(self._binary.checkFrame())
-        self.assertEqual(b"", self._binary.getFrame())
+        assert not self._binary.isFrameReady()
+        assert not self._binary.checkFrame()
+        assert not self._binary.getFrame()
 
     def test_binary_framer_transaction_full(self):
         """Test a full binary frame transaction"""
         msg = TEST_MESSAGE
         pack = msg[2:-3]
         self._binary.addToFrame(msg)
-        self.assertTrue(self._binary.checkFrame())
+        assert self._binary.checkFrame()
         result = self._binary.getFrame()
-        self.assertEqual(pack, result)
+        assert pack == result
         self._binary.advanceFrame()
 
     def test_binary_framer_transaction_half(self):
@@ -766,43 +754,43 @@ def test_binary_framer_transaction_half(self):
         msg2 = b"\x00\x00\x05\x85\xC9\x7d"
         pack = msg1[2:] + msg2[:-3]
         self._binary.addToFrame(msg1)
-        self.assertFalse(self._binary.checkFrame())
+        assert not self._binary.checkFrame()
         result = self._binary.getFrame()
-        self.assertEqual(b"", result)
+        assert not result
         self._binary.addToFrame(msg2)
-        self.assertTrue(self._binary.checkFrame())
+        assert self._binary.checkFrame()
         result = self._binary.getFrame()
-        self.assertEqual(pack, result)
+        assert pack == result
         self._binary.advanceFrame()
 
     def test_binary_framer_populate(self):
         """Test a binary frame packet build"""
         request = ModbusRequest()
         self._binary.populateResult(request)
-        self.assertEqual(0x00, request.unit_id)
+        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.unit_id = 0xFF
+        message.slave_id = 0xFF
         message.function_code = 0x01
         expected = b"\x7b\xff\x01\x81\x80\x7d"
         actual = self._binary.buildPacket(message)
-        self.assertEqual(expected, actual)
+        assert expected == actual
         ModbusRequest.encode = old_encode
 
     def test_binary_process_incoming_packet(self):
         """Test binary process incoming packet."""
         mock_data = TEST_MESSAGE
-        unit = 0x00
+        slave = 0x00
 
         def mock_callback(_mock_data):
             pass
 
-        self._binary.processIncomingPacket(mock_data, mock_callback, unit)
+        self._binary.processIncomingPacket(mock_data, mock_callback, slave)
 
         # Test failure:
-        self._binary.checkFrame = MagicMock(return_value=False)
-        self._binary.processIncomingPacket(mock_data, mock_callback, unit)
+        self._binary.checkFrame = mock.MagicMock(return_value=False)
+        self._binary.processIncomingPacket(mock_data, mock_callback, slave)
diff --git a/test/test_unix_socket.py b/test/test_unix_socket.py
index acb91f655..d41c1ab04 100755
--- a/test/test_unix_socket.py
+++ b/test/test_unix_socket.py
@@ -29,9 +29,9 @@ async def _helper_server(path_addon):
     """Run server."""
     datablock = ModbusSequentialDataBlock(0x00, [17] * 100)
     context = ModbusSlaveContext(
-        di=datablock, co=datablock, hr=datablock, ir=datablock, unit=1
+        di=datablock, co=datablock, hr=datablock, ir=datablock, slave=1
     )
-    asyncio.create_task(
+    asyncio.create_task(  # noqa: RUF006
         StartAsyncUnixServer(
             context=ModbusServerContext(slaves=context, single=True),
             path=PATH + path_addon,
@@ -46,14 +46,14 @@ async def _helper_server(path_addon):
 @pytest.mark.skipif(pytest.IS_WINDOWS, reason="Windows have a timeout problem.")
 @pytest.mark.parametrize("path_addon", ["_1"])
 async def test_unix_server(_mock_run_server):
-    """Run async server with unit domain socket."""
+    """Run async server with unix domain socket."""
     await asyncio.sleep(0.1)
 
 
 @pytest.mark.skipif(pytest.IS_WINDOWS, reason="Windows have a timeout problem.")
 @pytest.mark.parametrize("path_addon", ["_2"])
 async def test_unix_async_client(path_addon, _mock_run_server):
-    """Run async client with unit domain socket."""
+    """Run async client with unix domain socket."""
     await asyncio.sleep(1)
     client = AsyncModbusTcpClient(
         HOST + path_addon,
diff --git a/test/test_utilities.py b/test/test_utilities.py
index bb5aefafa..d19743b3d 100644
--- a/test/test_utilities.py
+++ b/test/test_utilities.py
@@ -1,6 +1,5 @@
 """Test utilities."""
 import struct
-import unittest
 
 from pymodbus.utilities import (
     checkCRC,
@@ -32,16 +31,29 @@ def __init__(self):
     g_1 = dict_property(_test_master, 4)
 
 
-class SimpleUtilityTest(unittest.TestCase):
+class TestUtility:
     """Unittest for the pymod.utilities module."""
 
-    def setUp(self):
+    def setup_method(self):
         """Initialize the test environment"""
-        self.data = struct.pack(">HHHH", 0x1234, 0x2345, 0x3456, 0x4567)
-        self.string = b"test the computation"
-        self.bits = [True, False, True, False, True, False, True, False]
-
-    def tearDown(self):
+        self.data = struct.pack(  # pylint: disable=attribute-defined-outside-init
+            ">HHHH", 0x1234, 0x2345, 0x3456, 0x4567
+        )
+        self.string = (  # pylint: disable=attribute-defined-outside-init
+            b"test the computation"
+        )
+        self.bits = [  # pylint: disable=attribute-defined-outside-init
+            True,
+            False,
+            True,
+            False,
+            True,
+            False,
+            True,
+            False,
+        ]
+
+    def teardown_method(self):
         """Clean up the test environment"""
         del self.bits
         del self.string
@@ -49,44 +61,44 @@ def tearDown(self):
     def test_dict_property(self):
         """Test all string <=> bit packing functions"""
         result = DictPropertyTester()
-        self.assertEqual(result.l_1, "a")
-        self.assertEqual(result.l_2, "b")
-        self.assertEqual(result.l_3, "c")
-        self.assertEqual(result.s_1, "a")
-        self.assertEqual(result.s_2, "b")
-        self.assertEqual(result.g_1, "d")
+        assert result.l_1 == "a"
+        assert result.l_2 == "b"
+        assert result.l_3 == "c"
+        assert result.s_1 == "a"
+        assert result.s_2 == "b"
+        assert result.g_1 == "d"
 
         for store in "l_1 l_2 l_3 s_1 s_2 g_1".split(" "):
             setattr(result, store, "x")
 
-        self.assertEqual(result.l_1, "x")
-        self.assertEqual(result.l_2, "x")
-        self.assertEqual(result.l_3, "x")
-        self.assertEqual(result.s_1, "x")
-        self.assertEqual(result.s_2, "x")
-        self.assertEqual(result.g_1, "x")
+        assert result.l_1 == "x"
+        assert result.l_2 == "x"
+        assert result.l_3 == "x"
+        assert result.s_1 == "x"
+        assert result.s_2 == "x"
+        assert result.g_1 == "x"
 
     def test_default_value(self):
         """Test all string <=> bit packing functions"""
-        self.assertEqual(default(1), 0)
-        self.assertEqual(default(1.1), 0.0)
-        self.assertEqual(default(1 + 1j), 0j)
-        self.assertEqual(default("string"), "")
-        self.assertEqual(default([1, 2, 3]), [])
-        self.assertEqual(default({1: 1}), {})
-        self.assertEqual(default(True), False)
+        assert not default(1)
+        assert not default(1.1)
+        assert not default(1 + 1)
+        assert not default("string")
+        assert default([1, 2, 3]) == []
+        assert default({1: 1}) == {}
+        assert not default(True)
 
     def test_bit_packing(self):
         """Test all string <=> bit packing functions"""
-        self.assertEqual(unpack_bitstring(b"\x55"), self.bits)
-        self.assertEqual(pack_bitstring(self.bits), b"\x55")
+        assert unpack_bitstring(b"\x55") == self.bits
+        assert pack_bitstring(self.bits) == b"\x55"
 
     def test_longitudinal_redundancycheck(self):
         """Test the longitudinal redundancy check code"""
-        self.assertTrue(checkLRC(self.data, 0x1C))
-        self.assertTrue(checkLRC(self.string, 0x0C))
+        assert checkLRC(self.data, 0x1C)
+        assert checkLRC(self.string, 0x0C)
 
     def test_cyclic_redundancy_check(self):
         """Test the cyclic redundancy check code"""
-        self.assertTrue(checkCRC(self.data, 0xE2DB))
-        self.assertTrue(checkCRC(self.string, 0x889E))
+        assert checkCRC(self.data, 0xE2DB)
+        assert checkCRC(self.string, 0x889E)
diff --git a/test/test_version.py b/test/test_version.py
deleted file mode 100644
index 17f329334..000000000
--- a/test/test_version.py
+++ /dev/null
@@ -1,27 +0,0 @@
-"""Test version."""
-import unittest
-
-from pymodbus import __version__ as pymodbus_version
-from pymodbus import __version_full__ as pymodbus_version_full
-from pymodbus.version import Version, version
-
-
-class ModbusVersionTest(unittest.TestCase):
-    """Unittest for the pymodbus._version code."""
-
-    def setUp(self):
-        """Initialize the test environment"""
-
-    def tearDown(self):
-        """Clean up the test environment"""
-
-    def test_version_class(self):
-        """Test version class."""
-        test_version = Version("test", 1, 2, 3, "sometag")
-        self.assertEqual(test_version.short(), "1.2.3.sometag")
-        self.assertEqual(str(test_version), "[test, version 1.2.3.sometag]")
-        self.assertEqual(test_version.package, "test")
-
-        self.assertEqual(pymodbus_version, version.short())
-        self.assertEqual(pymodbus_version_full, str(version))
-        self.assertEqual(version.package, "pymodbus")
diff --git a/test/transport/test_basic.py b/test/transport/test_basic.py
new file mode 100644
index 000000000..930c7aa94
--- /dev/null
+++ b/test/transport/test_basic.py
@@ -0,0 +1,504 @@
+"""Test transport."""
+import asyncio
+import os
+from unittest import mock
+
+import pytest
+from serial import SerialException
+
+from pymodbus.framer import ModbusFramer
+from pymodbus.transport.transport import BaseTransport
+
+
+class TestBaseTransport:
+    """Test transport module, base part."""
+
+    base_comm_name = "test comm"
+    base_reconnect_delay = 1
+    base_reconnect_delay_max = 3.5
+    base_timeout_connect = 2
+    base_framer = ModbusFramer
+    base_host = "test host"
+    base_port = 502
+    base_server_hostname = "server test host"
+    base_baudrate = 9600
+    base_bytesize = 8
+    base_parity = "e"
+    base_stopbits = 2
+    cwd = None
+
+    class dummy_transport(BaseTransport):
+        """Transport class for test."""
+
+        def __init__(self):
+            """Initialize."""
+            super().__init__(
+                TestBaseTransport.base_comm_name,
+                [
+                    TestBaseTransport.base_reconnect_delay * 1000,
+                    TestBaseTransport.base_reconnect_delay_max * 1000,
+                ],
+                TestBaseTransport.base_timeout_connect * 1000,
+                TestBaseTransport.base_framer,
+                None,
+                None,
+                None,
+            )
+            self.abort = mock.MagicMock()
+            self.close = mock.MagicMock()
+
+    @classmethod
+    async def setup_BaseTransport(cls):
+        """Create base object."""
+        base = BaseTransport(
+            cls.base_comm_name,
+            (cls.base_reconnect_delay * 1000, cls.base_reconnect_delay_max * 1000),
+            cls.base_timeout_connect * 1000,
+            cls.base_framer,
+            mock.MagicMock(),
+            mock.MagicMock(),
+            mock.MagicMock(),
+        )
+        params = base.CommParamsClass(
+            done=True,
+            comm_name=cls.base_comm_name,
+            reconnect_delay=cls.base_reconnect_delay,
+            reconnect_delay_max=cls.base_reconnect_delay_max,
+            timeout_connect=cls.base_timeout_connect,
+            framer=cls.base_framer,
+        )
+        cls.cwd = os.getcwd().split("/")[-1]
+        if cls.cwd == "transport":
+            cls.cwd = "../../"
+        elif cls.cwd == "test":
+            cls.cwd = "../"
+        else:
+            cls.cwd = ""
+        cls.cwd = cls.cwd + "examples/certificates/pymodbus."
+        return base, params
+
+    async def test_init(self):
+        """Test init()"""
+        base, params = await self.setup_BaseTransport()
+        params.done = False
+        assert base.comm_params == params
+
+        assert base.cb_connection_made
+        assert base.cb_connection_lost
+        assert base.cb_handle_data
+        assert not base.reconnect_delay_current
+        assert not base.reconnect_timer
+
+    async def test_property_done(self):
+        """Test done property"""
+        base, params = await self.setup_BaseTransport()
+        base.comm_params.check_done()
+        with pytest.raises(RuntimeError):
+            base.comm_params.check_done()
+
+    @pytest.mark.skipif(
+        pytest.IS_WINDOWS, reason="Windows do not support unix sockets."
+    )
+    @pytest.mark.parametrize("setup_server", [True, False])
+    async def test_properties_unix(self, setup_server):
+        """Test properties."""
+        base, params = await self.setup_BaseTransport()
+        base.setup_unix(setup_server, self.base_host)
+        params.host = self.base_host
+        assert base.comm_params == params
+        assert base.call_connect_listen
+
+    @pytest.mark.skipif(
+        not pytest.IS_WINDOWS, reason="Windows do not support unix sockets."
+    )
+    @pytest.mark.parametrize("setup_server", [True, False])
+    async def test_properties_unix_windows(self, setup_server):
+        """Test properties."""
+        base, params = await self.setup_BaseTransport()
+        with pytest.raises(RuntimeError):
+            base.setup_unix(setup_server, self.base_host)
+
+    @pytest.mark.parametrize("setup_server", [True, False])
+    async def test_properties_tcp(self, setup_server):
+        """Test properties."""
+        base, params = await self.setup_BaseTransport()
+        base.setup_tcp(setup_server, self.base_host, self.base_port)
+        params.host = self.base_host
+        params.port = self.base_port
+        assert base.comm_params == params
+        assert base.call_connect_listen
+
+    @pytest.mark.parametrize("setup_server", [True, False])
+    async def test_properties_udp(self, setup_server):
+        """Test properties."""
+        base, params = await self.setup_BaseTransport()
+        base.setup_udp(setup_server, self.base_host, self.base_port)
+        params.host = self.base_host
+        params.port = self.base_port
+        assert base.comm_params == params
+        assert base.call_connect_listen
+
+    @pytest.mark.parametrize("setup_server", [True, False])
+    @pytest.mark.parametrize("sslctx", [None, "test ctx"])
+    async def test_properties_tls(self, setup_server, sslctx):
+        """Test properties."""
+        base, params = await self.setup_BaseTransport()
+        with mock.patch("pymodbus.transport.transport.ssl.SSLContext"):
+            base.setup_tls(
+                setup_server,
+                self.base_host,
+                self.base_port,
+                sslctx,
+                None,
+                None,
+                None,
+                self.base_server_hostname,
+            )
+            params.host = self.base_host
+            params.port = self.base_port
+            params.server_hostname = self.base_server_hostname
+            params.ssl = sslctx if sslctx else base.comm_params.ssl
+            assert base.comm_params == params
+            assert base.call_connect_listen
+
+    @pytest.mark.parametrize("setup_server", [True, False])
+    async def test_properties_serial(self, setup_server):
+        """Test properties."""
+        base, params = await self.setup_BaseTransport()
+        base.setup_serial(
+            setup_server,
+            self.base_host,
+            self.base_baudrate,
+            self.base_bytesize,
+            self.base_parity,
+            self.base_stopbits,
+        )
+        params.host = self.base_host
+        params.baudrate = self.base_baudrate
+        params.bytesize = self.base_bytesize
+        params.parity = self.base_parity
+        params.stopbits = self.base_stopbits
+        assert base.comm_params == params
+        assert base.call_connect_listen
+
+    async def test_with_magic(self):
+        """Test magic."""
+        base, _params = await self.setup_BaseTransport()
+        base.close = mock.MagicMock()
+        async with base:
+            pass
+        base.close.assert_called_once()
+
+    async def test_str_magic(self):
+        """Test magic."""
+        base, _params = await self.setup_BaseTransport()
+        assert str(base) == f"BaseTransport({self.base_comm_name})"
+
+    async def test_connection_made(self):
+        """Test connection_made()."""
+        base, params = await self.setup_BaseTransport()
+        transport = self.dummy_transport()
+        base.connection_made(transport)
+        assert base.transport == transport
+        assert not base.recv_buffer
+        assert not base.reconnect_timer
+        assert base.reconnect_delay_current == params.reconnect_delay
+        base.cb_connection_made.assert_called_once()
+        base.cb_connection_lost.assert_not_called()
+        base.cb_handle_data.assert_not_called()
+        base.close()
+
+    async def test_connection_lost(self):
+        """Test connection_lost()."""
+        base, params = await self.setup_BaseTransport()
+        transport = self.dummy_transport()
+        base.connection_lost(transport)
+        assert not base.transport
+        assert not base.recv_buffer
+        assert not base.reconnect_timer
+        assert not base.reconnect_delay_current
+        base.cb_connection_made.assert_not_called()
+        base.cb_handle_data.assert_not_called()
+        base.cb_connection_lost.assert_called_once()
+        # reconnect is only after a successful connect
+        base.connection_made(transport)
+        base.connection_lost(transport)
+        assert base.reconnect_timer
+        assert not base.transport
+        assert not base.recv_buffer
+        assert base.reconnect_timer
+        assert base.reconnect_delay_current == 2 * params.reconnect_delay
+        base.cb_connection_lost.call_count == 2
+        base.close()
+        assert not base.reconnect_timer
+
+    async def test_eof_received(self):
+        """Test connection_lost()."""
+        base, params = await self.setup_BaseTransport()
+        self.dummy_transport()
+        base.eof_received()
+        assert not base.transport
+        assert not base.recv_buffer
+        assert not base.reconnect_timer
+        assert not base.reconnect_delay_current
+
+    async def test_close(self):
+        """Test close()."""
+        base, _params = await self.setup_BaseTransport()
+        transport = self.dummy_transport()
+        base.connection_made(transport)
+        base.cb_connection_made.reset_mock()
+        base.cb_connection_lost.reset_mock()
+        base.cb_handle_data.reset_mock()
+        base.recv_buffer = b"abc"
+        base.reconnect_timer = mock.MagicMock()
+        base.close()
+        transport.abort.assert_called_once()
+        transport.close.assert_called_once()
+        base.cb_connection_made.assert_not_called()
+        base.cb_connection_lost.assert_not_called()
+        base.cb_handle_data.assert_not_called()
+        assert not base.recv_buffer
+        assert not base.reconnect_timer
+
+    async def test_reset_delay(self):
+        """Test reset_delay()."""
+        base, _params = await self.setup_BaseTransport()
+        base.reconnect_delay_current = self.base_reconnect_delay + 1
+        base.reset_delay()
+        assert base.reconnect_delay_current == self.base_reconnect_delay
+
+    async def test_datagram(self):
+        """Test datagram_received()."""
+        base, _params = await self.setup_BaseTransport()
+        base.data_received = mock.MagicMock()
+        base.datagram_received(b"abc", "127.0.0.1")
+        base.data_received.assert_called_once()
+
+    async def test_data(self):
+        """Test data_received."""
+        base, _params = await self.setup_BaseTransport()
+        base.cb_handle_data = mock.MagicMock(return_value=2)
+        base.data_received(b"123456")
+        base.cb_handle_data.assert_called_once()
+        assert base.recv_buffer == b"3456"
+        base.data_received(b"789")
+        assert base.recv_buffer == b"56789"
+
+    async def test_send(self):
+        """Test send()."""
+        base, _params = await self.setup_BaseTransport()
+        base.transport = mock.AsyncMock()
+        await base.send(b"abc")
+
+    @pytest.mark.skipif(
+        pytest.IS_WINDOWS, reason="Windows do not support unix sockets."
+    )
+    async def test_connect_unix(self):
+        """Test connect_unix()."""
+        base, _params = await self.setup_BaseTransport()
+        base.setup_unix(False, self.base_host)
+        base.close = mock.Mock()
+        mocker = mock.AsyncMock()
+
+        base.loop.create_unix_connection = mocker
+        mocker.side_effect = FileNotFoundError("testing")
+        assert await base.transport_connect() == (None, None)
+        base.close.assert_called_once()
+        mocker.side_effect = None
+
+        mocker.return_value = (117, 118)
+        assert mocker.return_value == await base.transport_connect()
+        base.close.called_once()
+
+    async def test_connect_tcp(self):
+        """Test connect_tcp()."""
+        base, _params = await self.setup_BaseTransport()
+        base.setup_tcp(False, self.base_host, self.base_port)
+        base.close = mock.Mock()
+        mocker = mock.AsyncMock()
+
+        base.loop.create_connection = mocker
+        mocker.side_effect = asyncio.TimeoutError("testing")
+        assert await base.transport_connect() == (None, None)
+        base.close.assert_called_once()
+        mocker.side_effect = None
+
+        mocker.return_value = (117, 118)
+        assert mocker.return_value == await base.transport_connect()
+        base.close.assert_called_once()
+
+    async def test_connect_tls(self):
+        """Test connect_tcls()."""
+        base, _params = await self.setup_BaseTransport()
+        base.setup_tls(
+            False,
+            self.base_host,
+            self.base_port,
+            "no ssl",
+            None,
+            None,
+            None,
+            self.base_server_hostname,
+        )
+        base.close = mock.Mock()
+        mocker = mock.AsyncMock()
+
+        base.loop.create_connection = mocker
+        mocker.side_effect = asyncio.TimeoutError("testing")
+        assert await base.transport_connect() == (None, None)
+        base.close.assert_called_once()
+        mocker.side_effect = None
+
+        mocker.return_value = (117, 118)
+        assert mocker.return_value == await base.transport_connect()
+        base.close.assert_called_once()
+
+    async def test_connect_udp(self):
+        """Test connect_udp()."""
+        base, _params = await self.setup_BaseTransport()
+        base.setup_udp(False, self.base_host, self.base_port)
+        base.close = mock.Mock()
+        mocker = mock.AsyncMock()
+
+        base.loop.create_datagram_endpoint = mocker
+        mocker.side_effect = asyncio.TimeoutError("testing")
+        assert await base.transport_connect() == (None, None)
+        base.close.assert_called_once()
+        mocker.side_effect = None
+
+        mocker.return_value = (117, 118)
+        assert mocker.return_value == await base.transport_connect()
+        base.close.assert_called_once()
+
+    async def test_connect_serial(self):
+        """Test connect_serial()."""
+        base, _params = await self.setup_BaseTransport()
+        base.setup_serial(
+            False,
+            self.base_host,
+            self.base_baudrate,
+            self.base_bytesize,
+            self.base_parity,
+            self.base_stopbits,
+        )
+        base.close = mock.Mock()
+        mocker = mock.AsyncMock()
+
+        with mock.patch(
+            "pymodbus.transport.transport.create_serial_connection", new=mocker
+        ):
+            mocker.side_effect = asyncio.TimeoutError("testing")
+            assert await base.transport_connect() == (None, None)
+            base.close.assert_called_once()
+            mocker.side_effect = None
+
+            mocker.return_value = (117, 118)
+            assert mocker.return_value == await base.transport_connect()
+            base.close.assert_called_once()
+
+    @pytest.mark.skipif(
+        pytest.IS_WINDOWS, reason="Windows do not support unix sockets."
+    )
+    async def test_listen_unix(self):
+        """Test listen_unix()."""
+        base, _params = await self.setup_BaseTransport()
+        base.setup_unix(True, self.base_host)
+        base.close = mock.Mock()
+        mocker = mock.AsyncMock()
+
+        base.loop.create_unix_server = mocker
+        mocker.side_effect = OSError("testing")
+        assert await base.transport_listen() is None
+        base.close.assert_called_once()
+        mocker.side_effect = None
+
+        mocker.return_value = 117
+        assert mocker.return_value == await base.transport_listen()
+        base.close.assert_called_once()
+
+    async def test_listen_tcp(self):
+        """Test listen_tcp()."""
+        base, _params = await self.setup_BaseTransport()
+        base.setup_tcp(True, self.base_host, self.base_port)
+        base.close = mock.Mock()
+        mocker = mock.AsyncMock()
+
+        base.loop.create_server = mocker
+        mocker.side_effect = OSError("testing")
+        assert await base.transport_listen() is None
+        base.close.assert_called_once()
+        mocker.side_effect = None
+
+        mocker.return_value = 117
+        assert mocker.return_value == await base.transport_listen()
+        base.close.assert_called_once()
+
+    async def test_listen_tls(self):
+        """Test listen_tls()."""
+        base, _params = await self.setup_BaseTransport()
+        base.setup_tls(
+            True,
+            self.base_host,
+            self.base_port,
+            "no ssl",
+            None,
+            None,
+            None,
+            self.base_server_hostname,
+        )
+        base.close = mock.Mock()
+        mocker = mock.AsyncMock()
+
+        base.loop.create_server = mocker
+        mocker.side_effect = OSError("testing")
+        assert await base.transport_listen() is None
+        base.close.assert_called_once()
+        mocker.side_effect = None
+
+        mocker.return_value = 117
+        assert mocker.return_value == await base.transport_listen()
+        base.close.assert_called_once()
+
+    async def test_listen_udp(self):
+        """Test listen_udp()."""
+        base, _params = await self.setup_BaseTransport()
+        base.setup_udp(True, self.base_host, self.base_port)
+        base.close = mock.Mock()
+        mocker = mock.AsyncMock()
+
+        base.loop.create_datagram_endpoint = mocker
+        mocker.side_effect = OSError("testing")
+        assert await base.transport_listen() is None
+        base.close.assert_called_once()
+        mocker.side_effect = None
+
+        mocker.return_value = (117, 118)
+        assert await base.transport_listen() == 117
+        base.close.assert_called_once()
+
+    async def test_listen_serial(self):
+        """Test listen_serial()."""
+        base, _params = await self.setup_BaseTransport()
+        base.setup_serial(
+            True,
+            self.base_host,
+            self.base_baudrate,
+            self.base_bytesize,
+            self.base_parity,
+            self.base_stopbits,
+        )
+        base.close = mock.Mock()
+        mocker = mock.AsyncMock()
+
+        with mock.patch(
+            "pymodbus.transport.transport.create_serial_connection", new=mocker
+        ):
+            mocker.side_effect = SerialException("testing")
+            assert await base.transport_listen() is None
+            base.close.assert_called_once()
+            mocker.side_effect = None
+
+            mocker.return_value = 117
+            assert await base.transport_listen() == 117
+            base.close.assert_called_once()
diff --git a/test/transport/test_comm.py b/test/transport/test_comm.py
new file mode 100644
index 000000000..e810fa332
--- /dev/null
+++ b/test/transport/test_comm.py
@@ -0,0 +1,382 @@
+"""Test transport."""
+import asyncio
+import os
+import sys
+import time
+from tempfile import gettempdir
+
+import pytest
+
+from pymodbus.framer import ModbusFramer, ModbusSocketFramer
+from pymodbus.transport.transport import BaseTransport
+
+
+class TestCommTransport:
+    """Test for the transport module."""
+
+    cwd = None
+
+    @classmethod
+    def setup_CWD(cls):
+        """Get path to certificates."""
+        cls.cwd = os.getcwd().split("/")[-1]
+        if cls.cwd == "transport":
+            cls.cwd = "../../"
+        elif cls.cwd == "test":
+            cls.cwd = "../"
+        else:
+            cls.cwd = ""
+        cls.cwd = cls.cwd + "examples/certificates/pymodbus."
+
+    class dummy_transport(BaseTransport):
+        """Transport class for test."""
+
+        def cb_connection_made(self):
+            """Handle callback."""
+
+        def cb_connection_lost(self, _exc):
+            """Handle callback."""
+
+        def cb_handle_data(self, _data):
+            """Handle callback."""
+            return 0
+
+        def __init__(self, framer: ModbusFramer, comm_name="test comm"):
+            """Initialize."""
+            super().__init__(
+                comm_name,
+                [2500, 9000],
+                2000,
+                framer,
+                self.cb_connection_made,
+                self.cb_connection_lost,
+                self.cb_handle_data,
+            )
+
+    @pytest.mark.skipif(
+        pytest.IS_WINDOWS, reason="Windows do not support unix sockets."
+    )
+    @pytest.mark.xdist_group(name="server_serialize")
+    async def test_connect_unix(self):
+        """Test connect_unix()."""
+        client = self.dummy_transport(ModbusSocketFramer)
+        domain_socket = "/domain_unix"
+        client.setup_unix(False, domain_socket)
+        start = time.time()
+        assert await client.transport_connect() == (None, None)
+        delta = time.time() - start
+        assert delta < client.comm_params.timeout_connect * 1.2
+
+        client = self.dummy_transport(ModbusSocketFramer)
+        domain_socket = gettempdir() + "/domain_unix"
+        client.setup_unix(False, domain_socket)
+        start = time.time()
+        assert await client.transport_connect() == (None, None)
+        delta = time.time() - start
+        assert delta < client.comm_params.timeout_connect * 1.2
+
+    @pytest.mark.xdist_group(name="server_serialize")
+    async def test_connect_tcp(self):
+        """Test connect_tcp()."""
+        client = self.dummy_transport(ModbusSocketFramer)
+        client.setup_tcp(False, "142.250.200.78", 502)
+        start = time.time()
+        assert await client.transport_connect() == (None, None)
+        delta = time.time() - start
+        assert delta < client.comm_params.timeout_connect * 1.2
+
+        client = self.dummy_transport(ModbusSocketFramer)
+        client.setup_tcp(False, "localhost", 5001)
+        start = time.time()
+        assert await client.transport_connect() == (None, None)
+        delta = time.time() - start
+        assert delta < client.comm_params.timeout_connect * 1.2
+
+    @pytest.mark.xdist_group(name="server_serialize")
+    async def test_connect_tls(self):
+        """Test connect_tls()."""
+        self.setup_CWD()
+        client = self.dummy_transport(ModbusSocketFramer)
+        client.setup_tls(
+            False,
+            "142.250.200.78",
+            502,
+            None,
+            self.cwd + "crt",
+            self.cwd + "key",
+            None,
+            "localhost",
+        )
+        start = time.time()
+        assert await client.transport_connect() == (None, None)
+        delta = time.time() - start
+        assert delta < client.comm_params.timeout_connect * 1.2
+
+        client = self.dummy_transport(ModbusSocketFramer)
+        client.setup_tls(
+            False,
+            "127.0.0.1",
+            5001,
+            None,
+            self.cwd + "crt",
+            self.cwd + "key",
+            None,
+            "localhost",
+        )
+        start = time.time()
+        assert await client.transport_connect() == (None, None)
+        delta = time.time() - start
+        assert delta < client.comm_params.timeout_connect * 1.2
+
+    @pytest.mark.xdist_group(name="server_serialize")
+    async def test_connect_serial(self):
+        """Test connect_serial()."""
+        client = self.dummy_transport(ModbusSocketFramer)
+        client.setup_serial(
+            False,
+            "no_port",
+            9600,
+            8,
+            "E",
+            2,
+        )
+        start = time.time()
+        assert await client.transport_connect() == (None, None)
+        delta = time.time() - start
+        assert delta < client.comm_params.timeout_connect * 1.2
+
+        client = self.dummy_transport(ModbusSocketFramer)
+        client.setup_serial(
+            False,
+            "unix:/localhost:5001",
+            9600,
+            8,
+            "E",
+            2,
+        )
+        start = time.time()
+        assert await client.transport_connect() == (None, None)
+        delta = time.time() - start
+        assert delta < client.comm_params.timeout_connect * 1.2
+
+    @pytest.mark.skipif(
+        pytest.IS_WINDOWS, reason="Windows do not support unix sockets."
+    )
+    @pytest.mark.xdist_group(name="server_serialize")
+    async def test_listen_unix(self):
+        """Test listen_unix()."""
+        server = self.dummy_transport(ModbusSocketFramer)
+        domain_socket = "/test_unix_"
+        server.setup_unix(True, domain_socket)
+        assert not await server.transport_listen()
+        assert not server.transport
+
+        server = self.dummy_transport(ModbusSocketFramer)
+        domain_socket = gettempdir() + "/test_unix_" + str(time.time())
+        server.setup_unix(True, domain_socket)
+        assert await server.transport_listen()
+        assert server.transport
+        server.close()
+
+    @pytest.mark.xdist_group(name="server_serialize")
+    async def test_listen_tcp(self):
+        """Test listen_tcp()."""
+        server = self.dummy_transport(ModbusSocketFramer)
+        server.setup_tcp(True, "10.0.0.1", 5101)
+        assert not await server.transport_listen()
+        assert not server.transport
+
+        server = self.dummy_transport(ModbusSocketFramer)
+        server.setup_tcp(True, "localhost", 5101)
+        assert await server.transport_listen()
+        assert server.transport
+        server.close()
+
+    @pytest.mark.xdist_group(name="server_serialize")
+    async def test_listen_tls(self):
+        """Test listen_tls()."""
+        self.setup_CWD()
+        server = self.dummy_transport(ModbusSocketFramer)
+        server.setup_tls(
+            True,
+            "10.0.0.1",
+            5101,
+            None,
+            self.cwd + "crt",
+            self.cwd + "key",
+            None,
+            "localhost",
+        )
+        assert not await server.transport_listen()
+        assert not server.transport
+
+        server = self.dummy_transport(ModbusSocketFramer)
+        server.setup_tls(
+            True,
+            "127.0.0.1",
+            5101,
+            None,
+            self.cwd + "crt",
+            self.cwd + "key",
+            None,
+            "localhost",
+        )
+        assert await server.transport_listen()
+        assert server.transport
+        server.close()
+
+    @pytest.mark.xdist_group(name="server_serialize")
+    async def test_listen_udp(self):
+        """Test listen_udp()."""
+        server = self.dummy_transport(ModbusSocketFramer)
+        server.setup_udp(True, "10.0.0.1", 5101)
+        assert not await server.transport_listen()
+        assert not server.transport
+
+        server = self.dummy_transport(ModbusSocketFramer)
+        server.setup_udp(True, "localhost", 5101)
+        assert await server.transport_listen()
+        assert server.transport
+        server.close()
+
+    @pytest.mark.xdist_group(name="server_serialize")
+    async def test_listen_serial(self):
+        """Test listen_serial()."""
+        server = self.dummy_transport(ModbusSocketFramer)
+        server.setup_serial(
+            True,
+            "no port",
+            9600,
+            8,
+            "E",
+            2,
+        )
+        assert not await server.transport_listen()
+        assert not server.transport
+
+        # there are no positive test, since there are no standard tty port
+
+    @pytest.mark.skipif(
+        pytest.IS_WINDOWS, reason="Windows do not support unix sockets."
+    )
+    @pytest.mark.xdist_group(name="server_serialize")
+    async def test_connected_unix(self):
+        """Test listen/connect unix()."""
+        server_protocol = self.dummy_transport(ModbusSocketFramer)
+        domain_socket = gettempdir() + "/test_unix_" + str(time.time())
+        server_protocol.setup_unix(True, domain_socket)
+        await server_protocol.transport_listen()
+
+        client = self.dummy_transport(ModbusSocketFramer)
+        client.setup_unix(False, domain_socket)
+        assert await client.transport_connect() != (None, None)
+        client.close()
+        server_protocol.close()
+
+    @pytest.mark.xdist_group(name="server_serialize")
+    async def test_connected_tcp(self):
+        """Test listen/connect tcp()."""
+        server_protocol = self.dummy_transport(ModbusSocketFramer)
+        server_protocol.setup_tcp(True, "localhost", 5101)
+        assert await server_protocol.transport_listen()
+
+        client = self.dummy_transport(ModbusSocketFramer)
+        client.setup_tcp(False, "localhost", 5101)
+        assert await client.transport_connect() != (None, None)
+        client.close()
+        server_protocol.close()
+
+    @pytest.mark.xdist_group(name="server_serialize")
+    async def test_connected_tls(self):
+        """Test listen/connect tls()."""
+        self.setup_CWD()
+        server_protocol = self.dummy_transport(ModbusSocketFramer)
+        server_protocol.setup_tls(
+            True,
+            "127.0.0.1",
+            5102,
+            None,
+            self.cwd + "crt",
+            self.cwd + "key",
+            None,
+            "localhost",
+        )
+        assert await server_protocol.transport_listen()
+
+        client = self.dummy_transport(ModbusSocketFramer)
+        client.setup_tls(
+            False,
+            "127.0.0.1",
+            5102,
+            None,
+            self.cwd + "crt",
+            self.cwd + "key",
+            None,
+            "localhost",
+        )
+        assert await client.transport_connect() != (None, None)
+        client.close()
+        server_protocol.close()
+
+    @pytest.mark.xdist_group(name="server_serialize")
+    async def test_connected_udp(self):
+        """Test listen/connect udp()."""
+        server_protocol = self.dummy_transport(ModbusSocketFramer)
+        server_protocol.setup_udp(True, "localhost", 5101)
+        transport = await server_protocol.transport_listen()
+        assert transport
+
+        client = self.dummy_transport(ModbusSocketFramer)
+        client.setup_udp(False, "localhost", 5101)
+        assert await client.transport_connect() != (None, None)
+        client.close()
+        server_protocol.close()
+
+    @pytest.mark.xdist_group(name="server_serialize")
+    async def test_connected_serial(self):
+        """Test listen/connect serial()."""
+        server_protocol = self.dummy_transport(ModbusSocketFramer)
+        server_protocol.setup_tcp(True, "localhost", 5101)
+        assert await server_protocol.transport_listen()
+
+        client = self.dummy_transport(ModbusSocketFramer)
+        client.setup_serial(
+            False,
+            "unix:localhost:5001",
+            9600,
+            8,
+            "E",
+            2,
+        )
+        assert await client.transport_connect() == (None, None)
+        client.close()
+        server_protocol.close()
+
+    @pytest.mark.xdist_group(name="server_serialize")
+    async def test_connect_reconnect(self):
+        """Test connect() reconnecting."""
+        server = self.dummy_transport(ModbusSocketFramer, comm_name="server mode")
+        server.setup_tcp(True, "localhost", 5101)
+        await server.transport_listen()
+        assert server.transport
+
+        client = self.dummy_transport(ModbusSocketFramer, comm_name="client mode")
+        client.setup_tcp(False, "localhost", 5101)
+        assert await client.transport_connect() != (None, None)
+        server.close()
+        count = 100
+        while client.transport and count:
+            await asyncio.sleep(0.1)
+            count -= 1
+        if not sys.platform.startswith("win"):
+            assert not client.transport
+            assert client.reconnect_timer
+            assert (
+                client.reconnect_delay_current == 2 * client.comm_params.reconnect_delay
+            )
+            await asyncio.sleep(client.reconnect_delay_current * 1.2)
+            assert client.transport
+            assert client.reconnect_timer
+            assert client.reconnect_delay_current == client.comm_params.reconnect_delay
+        client.close()
+        server.close()
diff --git a/test/transport/test_data.py b/test/transport/test_data.py
new file mode 100644
index 000000000..035e3afb9
--- /dev/null
+++ b/test/transport/test_data.py
@@ -0,0 +1,55 @@
+"""Test transport."""
+import asyncio
+
+import pytest
+
+from pymodbus.framer import ModbusFramer, ModbusSocketFramer
+from pymodbus.transport.transport import BaseTransport
+
+
+class TestDataTransport:
+    """Test for the transport module."""
+
+    class dummy_transport(BaseTransport):
+        """Transport class for test."""
+
+        def cb_connection_made(self):
+            """Handle callback."""
+
+        def cb_connection_lost(self, _exc):
+            """Handle callback."""
+
+        def cb_handle_data(self, _data):
+            """Handle callback."""
+            return 0
+
+        def __init__(self, framer: ModbusFramer, comm_name="test comm"):
+            """Initialize."""
+            super().__init__(
+                comm_name,
+                [2500, 9000],
+                2000,
+                framer,
+                self.cb_connection_made,
+                self.cb_connection_lost,
+                self.cb_handle_data,
+            )
+
+    @pytest.mark.skipif(pytest.IS_WINDOWS, reason="Windows problem.")
+    @pytest.mark.xdist_group(name="server_serialize")
+    async def test_client_send(self):
+        """Test connect() reconnecting."""
+        server = self.dummy_transport(ModbusSocketFramer, comm_name="server mode")
+        server.setup_tcp(True, "localhost", 5101)
+        await server.transport_listen()
+        assert server.transport
+
+        client = self.dummy_transport(ModbusSocketFramer, comm_name="client mode")
+        client.setup_tcp(False, "localhost", 5101)
+        assert await client.transport_connect() != (None, None)
+        await client.send(b"ABC")
+        await asyncio.sleep(2)
+        assert server.recv_buffer == b"ABC"
+        await server.send(b"DEF")
+        await asyncio.sleep(2)
+        assert client.recv_buffer == b"DEF"