Showing preview only (207K chars total). Download the full file or copy to clipboard to get everything.
Repository: sourceperl/pyModbusTCP
Branch: master
Commit: 47675dcb865c
Files: 57
Total size: 192.7 KB
Directory structure:
gitextract_aihtbohh/
├── .github/
│ └── workflows/
│ └── tests.yml
├── .gitignore
├── .readthedocs.yaml
├── CHANGES
├── HOWTO-PyPi.md
├── HOWTO-pkg-devel.md
├── LICENSE
├── MANIFEST.in
├── README.rst
├── docs/
│ ├── conf.py
│ ├── examples/
│ │ ├── client_float.rst
│ │ ├── client_minimal.rst
│ │ ├── client_read_coils.rst
│ │ ├── client_read_h_registers.rst
│ │ ├── client_thread.rst
│ │ ├── client_write_coils.rst
│ │ ├── index.rst
│ │ ├── server.rst
│ │ ├── server_allow.rst
│ │ ├── server_change_log.rst
│ │ ├── server_schedule.rst
│ │ ├── server_serial_gw.rst
│ │ └── server_virtual_data.rst
│ ├── index.rst
│ ├── package/
│ │ ├── class_ModbusClient.rst
│ │ ├── class_ModbusServer.rst
│ │ ├── index.rst
│ │ └── module_utils.rst
│ ├── quickstart/
│ │ ├── index.rst
│ │ └── map.dot
│ └── requirements.txt
├── examples/
│ ├── README.md
│ ├── client_debug.py
│ ├── client_float.py
│ ├── client_minimal.py
│ ├── client_read_coils.py
│ ├── client_read_h_registers.py
│ ├── client_serial_gw.py
│ ├── client_thread.py
│ ├── client_write_coils.py
│ ├── server.py
│ ├── server_allow.py
│ ├── server_change_log.py
│ ├── server_schedule.py
│ ├── server_serial_gw.py
│ └── server_virtual_data.py
├── pyModbusTCP/
│ ├── __init__.py
│ ├── client.py
│ ├── constants.py
│ ├── server.py
│ └── utils.py
├── setup.cfg
├── setup.py
└── tests/
├── test_client.py
├── test_client_server.py
├── test_server.py
└── test_utils.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/tests.yml
================================================
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
name: Tests
on:
push:
branches: [ master ]
workflow_dispatch:
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
os: [ubuntu-latest, macOS-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flake8 pytest setuptools
python setup.py install
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics --extend-exclude=build/,docs/
- name: Test with pytest
run: |
pytest
================================================
FILE: .gitignore
================================================
/MANIFEST
/build/
/dist/
/lab/
*.pyc
/*.egg-info/
/.idea/
/.vscode/
/venv/
================================================
FILE: .readthedocs.yaml
================================================
# .readthedocs.yaml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Set the version of Python and other tools you might need
build:
os: ubuntu-22.04
tools:
python: "3.11"
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/conf.py
# We recommend specifying your dependencies to enable reproducible builds:
# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
python:
install:
- requirements: docs/requirements.txt
# Additional formats to be build (in addition to default HTML)
formats:
- epub
- htmlzip
- pdf
================================================
FILE: CHANGES
================================================
Revision history for pyModbusTCP
0.3.1.dev0 xxxx-xx-xx
- fix ModbusServer: debug messages now include OSError exceptions produced by getpeername (thanks to Pär Åhlund).
0.3.0 2024-09-04
- pyModbusTCP.client: now use the standard logging method as in the server part.
- ModbusClient: debug flag is removed (see examples/client_debug.py).
0.2.2 2024-07-31
- fix ModbusServer: wrong check of discrete inputs length in DataBank (thanks to OTnetproj).
- updated compatibility test (python versions): remove 3.7, add 3.12.
0.2.1 2023-11-21
- fix ModbusServer: wrong check of input registers length in DataBank (thanks to monsieurvor).
- improve hostname validation in utils (thanks to MCXIV and schmocker).
- experimental add of read_device_identification() (over modbus encapsulated interface) to client and server.
- experimental add of write_read_multiple_registers() function (code 0x17) to client and server (thanks to tanj).
- updated compatibility test (python versions): remove 3.5/3.6, add 3.11.
0.2.0 2022-06-05
- ModbusClient: parameters are now properties instead of methods (more intuitive).
- ModbusClient: now TCP auto open mode is active by default (auto_open=True, auto_close=False).
- ModbusClient: add custom_request method for send user define PDU to modbus server.
- ModbusClient: remove RTU mode.
- ModbusClient: clarify some things (private methods and internal vars rename).
- ModbusServer: big redesign to improve readability and maintainability.
- ModbusServer: no longer use hard linked class DataBank (it now return a deprecation warn).
- ModbusServer: add of ModbusServerDataHandler and DefaultDataBank class for easy customize.
- add or update server examples for new modbus server design.
- some updates on tests, fix random arrays generation on client test part.
- clarify usage of root privilege for open tcp/502 (avoid Errno 13) in server example.
- python 2 end of support.
0.1.10 2021-03-02
- word_list_to_long() and long_list_to_word(), now support 64 bits long long with opt long_long.
- encode_ieee() and decode_ieee(), now support double-precision format with opt double.
- add shortcut alias for functions with long names in utils.
- rewrite of some functions in utils.
- improve test_utils readability.
- server DataBank enforce type check on set data methods to avoid server crash (thanks to xuantw).
- now get_2comp can deal with negative python int.
- remove reference to devel github branch.
- improve last_error_txt() and last_except_txt().
0.1.9 2021-02-26
- add module error MB_SOCK_CLOSE_ERR (occur if frame send on close socket).
- add modbus exception EXP_NEGATIVE_ACKNOWLEDGE (code 0x07) to constants.
- add last_error_txt() and last_except_txt() for produce human readable status.
- add EXP_TXT, EXP_DETAILS and MB_ERR_TXT dicts to constants (text representation of codes).
- update of the compatibility test for python version: remove 2.6/3.2/3.3, add 3.7/3.8/3.9.
- conform to PEP 396 (add pyModbusTCP.__version__ field).
0.1.8 2018-10-15
- fix ModbusServer: avoid hard coded TCP port (thanks to akobyl).
- add stop() and is_run property to ModbusServer (thanks to Rugiewitz).
0.1.7 2018-08-20
- fix ModbusServer issue on Windows (thanks to andreascian).
0.1.6 2018-05-14
- fix multiple TCP packets recv issue in ModbusClient and ModbusServer (thanks Farin94).
0.1.5 2017-11-23
- add long_list_to_word to utils.
- add float support as example.
0.1.4 2017-11-13
- fix port and host accessors, change check now use "==" and not "is".
0.1.3 2017-09-29
- setup now use setuptools.
0.1.2 2017-09-28
- fix 'Rx' label on error.
- change file mode for server.py example.
- fix compatibility with modbus unit_id = 0 (thanks to mfkenney).
- fix compatibility for modbus frame with garbage.
0.1.1 2016-05-30
- add toggle_bit() to utils.
- add server.py example.
- add HOWTO for safe PyPI upload.
0.1.0 2016-05-30
- fix some issues in PEP 8 conformance and inline doc.
- client modbus RTU: move crc16 compute to utils.
- add write_multiple_coils() function (code 0x0f).
- add test_bit(), set_bit(), reset_bit() to utils.
- add a modbus/TCP multithreaded server through ModbusServer class (for test only).
0.0.13 2015-12-24
- add auto_open and auto_close to README.rst and quickstart doc.
- add example min_read_bit.py for show minimal code approach.
0.0.12 2015-12-11
- add auto_open and auto_close mode to ModbusClient.
- add accessor function for socket timeout.
- close TCP socket if hostname change
- close TCP socket if port change
0.0.11 2015-03-27
- fix sock.settimeout missing (thanks to bonaime).
- fix PEP 8 style (thanks to bonaime).
0.0.10 2015-01-22
- Add timeout parameter to ModbusClient constructor (thanks to bonaime).
0.0.9 2014-10-10
- Fix rx_byte_count check in ModbusClient class.
0.0.8 2014-09-23
- Catch excepts on socket send/recv.
- Sphinx documentation include a quickstart and examples.
0.0.7 2014-08-31
- Add unit test (see test/).
- Add params host/port/unit_id/debug on ModbusClient constructor.
- Add utils module for modbus data mangling.
0.0.6 2014-08-25
- Fix "socket error" message when call open() on dual stack IPv6/4 host.
- Check rx byte count field in functions 1 to 4.
- Fix max bit number problem in functions 1 and 2 (from 125 to 2000).
- Add debug message, if _send() call on close socket.
- Rename module name from const to constants.
- Update MANIFEST.in to remove docs and examples from sdist archive.
- Update README.rst sample code for Python3.
0.0.5 2014-08-08
- Now deal with IPv6 host.
- Fix Python3 issue in _crc().
- Improve modbus RTU receive code.
- Secure frame size before struct.unpack invocation.
0.0.4 2014-08-07
- Add class documentation (add doc/ and sphinx/ directory).
- Add sphinx docstring in client.py.
0.0.3 2014-08-05
- Fix padding problem in write_single_coil().
- Add new examples.
0.0.2 2014-08-05
- Compatibility with Python 3 and 2.7.
- Use RST format for README instead of markdown, now set long_description.
- Add a MANIFEST.in file and include examples/ on sdist.
0.0.1 2014-08-04
- First release of pyModbusTCP.
================================================
FILE: HOWTO-PyPi.md
================================================
## How to upload on PyPI
Here we use the twine tool to do the job, see [Twine setup](#twine-setup) to add and configure it.
### build archive and wheel
```bash
python setup.py sdist bdist_wheel
```
### upload archive and wheel to PyPi test server
```bash
twine upload dist/pyModbusTCP-x.x.x* -r pypitest
```
Check result at https://test.pypi.org/project/pyModbusTCP/.
### upload archive and wheel to PyPi server
```bash
twine upload dist/pyModbusTCP-x.x.x* -r pypi
```
Check result at https://pypi.python.org/project/pyModbusTCP/.
## Twine setup
### install twine
```bash
sudo pip install twine
```
### create it's conf file
Create ~/.pypirc with credentials for pypi and pypitest.
```bash
cat <<EOT >> ~/.pypirc
[distutils]
index-servers =
pypi
pypitest
[pypi]
repository: https://upload.pypi.org/legacy/
username: __token__
password: mytoken
[pypitest]
repository: https://test.pypi.org/legacy/
username: __token__
password: mytoken
EOT
```
Update it with valid credentials.
```bash
nano ~/.pypirc
```
================================================
FILE: HOWTO-pkg-devel.md
================================================
## How to set package developer mode (also call editable mode on pip)
*After set this, we can directly test effect of editing a package files
without need to fully reinstall it.*
Turn on develop mode (add current package files to python path) in a virtual env:
```bash
python -m venv venv && source venv/bin/activate
pip install --editable .
```
Turn off:
```bash
pip uninstall pyModbusTCP
```
View the current python path:
```bash
python -c 'import sys; print(sys.path)'
```
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2014 l.lefebvre
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: MANIFEST.in
================================================
include README.rst
include setup.py
include CHANGES
include LICENSE
recursive-include pyModbusTCP *.py
================================================
FILE: README.rst
================================================
.. |badge_tests| image:: https://github.com/sourceperl/pyModbusTCP/actions/workflows/tests.yml/badge.svg?branch=master
:target: https://github.com/sourceperl/pyModbusTCP/actions/workflows/tests.yml
.. |badge_docs| image:: https://readthedocs.org/projects/pymodbustcp/badge/?version=latest
:target: http://pymodbustcp.readthedocs.io/
pyModbusTCP |badge_tests| |badge_docs|
======================================
A simple Modbus/TCP client library for Python.
pyModbusTCP is pure Python code without any extension or external module dependency.
Since version 0.1.0, a server is also available.
Tests
-----
The module is currently test on Python 3.8, 3.9, 3.10, 3.11 and 3.12.
For Linux, Mac OS and Windows.
Documentation
-------------
Documentation of the last release is available online at https://pymodbustcp.readthedocs.io/.
Setup
-----
You can install this package from:
PyPI, the easy way:
.. code-block:: bash
# install the last available release (stable)
sudo pip install pyModbusTCP
.. code-block:: bash
# install a specific version (here release v0.1.10)
sudo pip install pyModbusTCP==v0.1.10
From GitHub:
.. code-block:: bash
# install a specific version (here release v0.1.10) directly from github servers
sudo pip install git+https://github.com/sourceperl/pyModbusTCP.git@v0.1.10
Note on the use of versions:
Over time, some things can change. So, it's a good practice that you always use a specific version of a package for
your project, instead of just relying on the default behavior. Without precision, the installation tools will always
install the latest version available for a package, this may have some drawbacks. For example, in pyModbusTCP, the TCP
automatic open mode will be active by default from version 0.2.0. It is not the case with previous versions and it just
doesn't exist before the 0.0.12. This can lead to some strange behaviour of your application if you are not aware of
the change. Look at `CHANGES <https://github.com/sourceperl/pyModbusTCP/blob/master/CHANGES>`_ for details on versions
available.
Usage example
-------------
See examples/ for full scripts.
include (for all samples)
~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from pyModbusTCP.client import ModbusClient
module init (TCP always open)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
# TCP auto connect on first modbus request
c = ModbusClient(host="localhost", port=502, unit_id=1, auto_open=True)
module init (TCP open/close for each request)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
# TCP auto connect on modbus request, close after it
c = ModbusClient(host="127.0.0.1", auto_open=True, auto_close=True)
Read 2x 16 bits registers at modbus address 0 :
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
regs = c.read_holding_registers(0, 2)
if regs:
print(regs)
else:
print("read error")
Write value 44 and 55 to registers at modbus address 10 :
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
if c.write_multiple_registers(10, [44,55]):
print("write ok")
else:
print("write error")
================================================
FILE: docs/conf.py
================================================
# -*- coding: utf-8 -*-
#
# pyModbusTCP documentation build configuration file, created by
# sphinx-quickstart on Thu Sep 28 18:36:32 2017.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import sys
# Insert pyModbusTCP path to read current version in it
sys.path.insert(0, os.path.abspath('..'))
import pyModbusTCP
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ['sphinx.ext.autodoc', 'sphinx_rtd_theme']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The encoding of source files.
#
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'pyModbusTCP'
copyright = u'2023, Loïc Lefebvre'
author = u'Loïc Lefebvre'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = pyModbusTCP.__version__
# The full version, including alpha/beta/rc tags.
release = pyModbusTCP.__version__
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = 'en'
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#
# today = ''
#
# Else, today_fmt is used as the format for a strftime call.
#
# today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The reST default role (used for this markup: `text`) to use for all
# documents.
#
# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#
# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#
# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
# keep_warnings = False
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'sphinx_rtd_theme'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {
# 'sidebar_width': '240px',
# }
# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []
# The name for this set of Sphinx documents.
# "<project> v<release> documentation" by default.
#
# html_title = u'pyModbusTCP v0.1.2'
# A shorter title for the navigation bar. Default is the same as html_title.
#
# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#
# html_logo = None
# The name of an image file (relative to this directory) to use as a favicon of
# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#
# html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
# html_static_path = ['_static']
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
#
# html_extra_path = []
# If not None, a 'Last updated on:' timestamp is inserted at every page
# bottom, using the given strftime format.
# The empty string is equivalent to '%b %d, %Y'.
#
# html_last_updated_fmt = None
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#
# html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#
# html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#
# html_additional_pages = {}
# If false, no module index is generated.
#
# html_domain_indices = True
# If false, no index is generated.
#
# html_use_index = True
# If true, the index is split into individual pages for each letter.
#
# html_split_index = False
# If true, links to the reST sources are added to the pages.
#
# html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#
# html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#
# html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#
# html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
# html_file_suffix = None
# Language to be used for generating the HTML full-text search index.
# Sphinx supports the following languages:
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh'
#
# html_search_language = 'en'
# A dictionary with options for the search language support, empty by default.
# 'ja' uses this config value.
# 'zh' user can custom change `jieba` dictionary path.
#
# html_search_options = {'type': 'default'}
# The name of a javascript file (relative to the configuration directory) that
# implements a search results scorer. If empty, the default will be used.
#
# html_search_scorer = 'scorer.js'
# Output file base name for HTML help builder.
htmlhelp_basename = 'pyModbusTCPdoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'pyModbusTCP.tex', u'pyModbusTCP Documentation',
u'Loïc Lefebvre', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#
# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#
# latex_use_parts = False
# If true, show page references after internal links.
#
# latex_show_pagerefs = False
# If true, show URL addresses after external links.
#
# latex_show_urls = False
# Documents to append as an appendix to all manuals.
#
# latex_appendices = []
# It false, will not define \strong, \code, itleref, \crossref ... but only
# \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added
# packages.
#
# latex_keep_old_macro_names = True
# If false, no module index is generated.
#
# latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'pymodbustcp', u'pyModbusTCP Documentation',
[author], 1)
]
# If true, show URL addresses after external links.
#
# man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'pyModbusTCP', u'pyModbusTCP Documentation',
author, 'pyModbusTCP', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#
# texinfo_appendices = []
# If false, no module index is generated.
#
# texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#
# texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
#
# texinfo_no_detailmenu = False
================================================
FILE: docs/examples/client_float.rst
================================================
===============================
Client: add float (inheritance)
===============================
.. literalinclude:: ../../examples/client_float.py
================================================
FILE: docs/examples/client_minimal.rst
================================================
====================
Client: minimal code
====================
.. literalinclude:: ../../examples/client_minimal.py
================================================
FILE: docs/examples/client_read_coils.rst
================================================
==================
Client: read coils
==================
.. literalinclude:: ../../examples/client_read_coils.py
================================================
FILE: docs/examples/client_read_h_registers.rst
================================================
==============================
Client: read holding registers
==============================
.. literalinclude:: ../../examples/client_read_h_registers.py
================================================
FILE: docs/examples/client_thread.rst
================================================
======================
Client: polling thread
======================
.. literalinclude:: ../../examples/client_thread.py
================================================
FILE: docs/examples/client_write_coils.rst
================================================
===================
Client: write coils
===================
.. literalinclude:: ../../examples/client_write_coils.py
================================================
FILE: docs/examples/index.rst
================================================
pyModbusTCP examples
====================
*Here some examples to see pyModbusTCP in some use cases*
.. toctree::
:maxdepth: 2
client_minimal
client_read_coils
client_read_h_registers
client_write_coils
client_float
client_thread
server
server_allow
server_change_log
server_serial_gw
server_schedule
server_virtual_data
================================================
FILE: docs/examples/server.rst
================================================
===================
Server: basic usage
===================
.. literalinclude:: ../../examples/server.py
================================================
FILE: docs/examples/server_allow.rst
================================================
==========================
Server: with an allow list
==========================
.. literalinclude:: ../../examples/server_allow.py
================================================
FILE: docs/examples/server_change_log.rst
================================================
==========================
Server: with change logger
==========================
.. literalinclude:: ../../examples/server_change_log.py
================================================
FILE: docs/examples/server_schedule.rst
================================================
===============================
Server: schedule and alive word
===============================
.. literalinclude:: ../../examples/server_schedule.py
================================================
FILE: docs/examples/server_serial_gw.rst
================================================
=================================
Server: Modbus/TCP serial gateway
=================================
.. literalinclude:: ../../examples/server_serial_gw.py
================================================
FILE: docs/examples/server_virtual_data.rst
================================================
====================
Server: virtual data
====================
.. literalinclude:: ../../examples/server_virtual_data.py
================================================
FILE: docs/index.rst
================================================
Welcome to pyModbusTCP's documentation
======================================
.. toctree::
:maxdepth: 2
quickstart/index.rst
package/index.rst
examples/index.rst
================================================
FILE: docs/package/class_ModbusClient.rst
================================================
Module pyModbusTCP.client
=========================
.. automodule:: pyModbusTCP.client
*This module provide the ModbusClient class used to deal with modbus server.*
class ModbusClient
------------------
.. autoclass:: ModbusClient
:members:
:special-members: __init__
class DeviceIdentificationResponse
----------------------------------
.. autoclass:: DeviceIdentificationResponse
:members:
:special-members: __init__
================================================
FILE: docs/package/class_ModbusServer.rst
================================================
Module pyModbusTCP.server
=========================
.. automodule:: pyModbusTCP.server
*This module provide the class for the modbus server, it's data handler interface and finally the data bank.*
class ModbusServer
------------------
.. autoclass:: ModbusServer
:members:
.. automethod:: __init__
class DataHandler
-----------------
.. autoclass:: DataHandler
:members:
:special-members: __init__
class DataBank
--------------
.. autoclass:: DataBank
:members:
:special-members: __init__
class DeviceIdentification
--------------------------
.. autoclass:: DeviceIdentification
:members:
:special-members: __init__
================================================
FILE: docs/package/index.rst
================================================
pyModbusTCP modules documentation
=================================
Contents:
.. toctree::
:maxdepth: 2
class_ModbusClient
class_ModbusServer
module_utils
================================================
FILE: docs/package/module_utils.rst
================================================
Module pyModbusTCP.utils
========================
*This module provide a set of functions for modbus data mangling.*
Bit functions
-------------
.. automodule:: pyModbusTCP.utils
:members: byte_length, get_bits_from_int, reset_bit, set_bit, test_bit, toggle_bit
Word functions
--------------
.. automodule:: pyModbusTCP.utils
:members: long_list_to_word, word_list_to_long
Two's complement functions
--------------------------
.. automodule:: pyModbusTCP.utils
:members: get_2comp, get_list_2comp
IEEE floating-point functions
-----------------------------
.. automodule:: pyModbusTCP.utils
:members: decode_ieee, encode_ieee
Misc functions
--------------
.. automodule:: pyModbusTCP.utils
:members: crc16, valid_host
================================================
FILE: docs/quickstart/index.rst
================================================
Quick start guide
=================
Overview of the package
-----------------------
pyModbusTCP give access to modbus/TCP server through the ModbusClient object.
This class is define in the client module.
Since version 0.1.0, a server is available as ModbusServer class. This server
is currently in test (API can change at any time).
To deal with frequent need of modbus data mangling (for example convert 32 bits
IEEE float to 2x16 bits words) a special module named utils provide some helpful
functions.
**Package map:**
.. image:: map.png
:scale: 75 %
Package setup
-------------
from PyPi::
# install the last available version (stable)
sudo pip3 install pyModbusTCP
# or upgrade from an older version
sudo pip3 install pyModbusTCP --upgrade
# you can also install a specific version (here v0.1.10)
sudo pip3 install pyModbusTCP==v0.1.10
from GitHub::
git clone https://github.com/sourceperl/pyModbusTCP.git
cd pyModbusTCP
# here change "python" by your python target(s) version(s) (like python3.9)
sudo python setup.py install
ModbusClient: init
------------------
Init module from constructor (raise ValueError if host/port error)::
from pyModbusTCP.client import ModbusClient
try:
c = ModbusClient(host='localhost', port=502)
except ValueError:
print("Error with host or port params")
Or with properties::
from pyModbusTCP.client import ModbusClient
c = ModbusClient()
c.host = 'localhost'
c.port = 502
ModbusClient: TCP link management
---------------------------------
Since version 0.2.0, "auto open" mode is the default behaviour to deal with TCP open/close.
The "auto open" mode keep the TCP connection always open, so the default constructor is::
c = ModbusClient(host="localhost", auto_open=True, auto_close=False)
It's also possible to open/close TCP socket before and after each request::
c = ModbusClient(host="localhost", auto_open=True, auto_close=True)
Another way to deal with connection is to manually set it. Like this::
c = ModbusClient(host="localhost", auto_open=False, auto_close=False)
# open the socket for 2 reads then close it.
if c.open():
regs_list_1 = c.read_holding_registers(0, 10)
regs_list_2 = c.read_holding_registers(55, 10)
c.close()
ModbusClient: available modbus requests functions
-------------------------------------------------
See http://en.wikipedia.org/wiki/Modbus for full table.
+------------+------------------------------+---------------+--------------------------------------------------------------------------+
| Domain | Function name | Function code | ModbusClient function |
+============+==============================+===============+==========================================================================+
| Bit | Read Discrete Inputs | 2 | :py:meth:`~pyModbusTCP.client.ModbusClient.read_discrete_inputs` |
| +------------------------------+---------------+--------------------------------------------------------------------------+
| | Read Coils | 1 | :py:meth:`~pyModbusTCP.client.ModbusClient.read_coils` |
| +------------------------------+---------------+--------------------------------------------------------------------------+
| | Write Single Coil | 5 | :py:meth:`~pyModbusTCP.client.ModbusClient.write_single_coil` |
| +------------------------------+---------------+--------------------------------------------------------------------------+
| | Write Multiple Coils | 15 | :py:meth:`~pyModbusTCP.client.ModbusClient.write_multiple_coils` |
+------------+------------------------------+---------------+--------------------------------------------------------------------------+
| Register | Read Input Registers | 4 | :py:meth:`~pyModbusTCP.client.ModbusClient.read_input_registers` |
| +------------------------------+---------------+--------------------------------------------------------------------------+
| | Read Holding Registers | 3 | :py:meth:`~pyModbusTCP.client.ModbusClient.read_holding_registers` |
| +------------------------------+---------------+--------------------------------------------------------------------------+
| | Write Single Register | 6 | :py:meth:`~pyModbusTCP.client.ModbusClient.write_single_register` |
| +------------------------------+---------------+--------------------------------------------------------------------------+
| | Write Multiple Registers | 16 | :py:meth:`~pyModbusTCP.client.ModbusClient.write_multiple_registers` |
| +------------------------------+---------------+--------------------------------------------------------------------------+
| | Read/Write Multiple Registers| 23 | :py:meth:`~pyModbusTCP.client.ModbusClient.write_read_multiple_registers`|
| +------------------------------+---------------+--------------------------------------------------------------------------+
| | Mask Write Register | 22 | n/a |
+------------+------------------------------+---------------+--------------------------------------------------------------------------+
| File | Read FIFO Queue | 24 | n/a |
| +------------------------------+---------------+--------------------------------------------------------------------------+
| | Read File Record | 20 | n/a |
| +------------------------------+---------------+--------------------------------------------------------------------------+
| | Write File Record | 21 | n/a |
| +------------------------------+---------------+--------------------------------------------------------------------------+
| | Read Exception Status | 7 | n/a |
+------------+------------------------------+---------------+--------------------------------------------------------------------------+
| Diagnostic | Diagnostic | 8 | n/a |
| +------------------------------+---------------+--------------------------------------------------------------------------+
| | Get Com Event Counter | 11 | n/a |
| +------------------------------+---------------+--------------------------------------------------------------------------+
| | Get Com Event Log | 12 | n/a |
| +------------------------------+---------------+--------------------------------------------------------------------------+
| | Report Slave ID | 17 | n/a |
| +------------------------------+---------------+--------------------------------------------------------------------------+
| | Read Device Identification | 43 | :py:meth:`~pyModbusTCP.client.ModbusClient.read_device_identification` |
+------------+------------------------------+---------------+--------------------------------------------------------------------------+
ModbusClient: how-to debug
--------------------------
If need, you can enable debug log for ModbusClient like this::
import logging
from pyModbusTCP.client import ModbusClient
# set debug level for pyModbusTCP.client to see frame exchanges
logging.basicConfig()
logging.getLogger('pyModbusTCP.client').setLevel(logging.DEBUG)
c = ModbusClient(host="localhost", port=502)
when debug level is set, all debug messages are displayed on the console::
c.read_coils(0)
will give us the following result::
DEBUG:pyModbusTCP.client:(localhost:502:1) Tx [8F 8A 00 00 00 06 01] 01 00 00 00 01
DEBUG:pyModbusTCP.client:(localhost:502:1) Rx [8F 8A 00 00 00 04 01] 01 01 00
utils module: Modbus data mangling
----------------------------------
When we have to deal with the variety types of registers of PLC device, we often
need some data mangling. Utils part of pyModbusTCP can help you in this task.
Now, let's see some use cases.
- deal with negative numbers (two's complement)::
from pyModbusTCP import utils
list_16_bits = [0x0000, 0xFFFF, 0x00FF, 0x8001]
# show "[0, -1, 255, -32767]"
print(utils.get_list_2comp(list_16_bits, 16))
# show "-1"
print(utils.get_2comp(list_16_bits[1], 16))
More at http://en.wikipedia.org/wiki/Two%27s_complement
- convert integer of val_size bits (default is 16) to an array of boolean::
from pyModbusTCP import utils
# show "[True, False, True, False, False, False, False, False]"
print(utils.get_bits_from_int(0x05, val_size=8))
- read of 32 bits registers (also know as long format)::
from pyModbusTCP import utils
list_16_bits = [0x0123, 0x4567, 0xdead, 0xbeef]
# big endian sample (default)
list_32_bits = utils.word_list_to_long(list_16_bits)
# show "['0x1234567', '0xdeadbeef']"
print([hex(i) for i in list_32_bits])
# little endian sample
list_32_bits = utils.word_list_to_long(list_16_bits, big_endian=False)
# show "['0x45670123', '0xbeefdead']"
print([hex(i) for i in list_32_bits])
- IEEE single/double precision floating-point::
from pyModbusTCP import utils
# 32 bits IEEE single precision
# encode : python float 0.3 -> int 0x3e99999a
# display "0x3e99999a"
print(hex(utils.encode_ieee(0.3)))
# decode: python int 0x3e99999a -> float 0.3
# show "0.300000011921" (it's not 0.3, precision leak with float...)
print(utils.decode_ieee(0x3e99999a))
# 64 bits IEEE double precision
# encode: python float 6.62606957e-34 -> int 0x390b860bb596a559
# display "0x390b860bb596a559"
print(hex(utils.encode_ieee(6.62606957e-34, double=True)))
# decode: python int 0x390b860bb596a559 -> float 6.62606957e-34
# display "6.62606957e-34"
print(utils.decode_ieee(0x390b860bb596a559, double=True))
================================================
FILE: docs/quickstart/map.dot
================================================
/*
generate pyModbusTCP map as PNG
(need sudo apt-get install graphviz)
command:
dot -Tpng map.dot > map.png
*/
digraph pyModbusTCP_map {
"pyModbusTCP" -> "client";
"pyModbusTCP" -> "server";
"pyModbusTCP" -> "utils";
"pyModbusTCP" -> "constants";
"client" -> "class ModbusClient";
"server" -> "class ModbusServer";
"utils" -> "data mangling functions";
"constants" -> "all package constants";
}
================================================
FILE: docs/requirements.txt
================================================
sphinx-rtd-theme==1.3.0
================================================
FILE: examples/README.md
================================================
## Important notice
**The examples in this directory are designed to work with the version of pyModbusTCP currently in this repository. They
may or may not work with the PyPi version which is always the latest stable version and not the development one.**
================================================
FILE: examples/client_debug.py
================================================
#!/usr/bin/env python3
""" An example of basic logging for ModbusClient debugging purposes. """
import logging
import time
from pyModbusTCP.server import ModbusServer
from pyModbusTCP.client import ModbusClient
# a logger for this script
logger = logging.getLogger(__name__)
# global log conf: sets a default format and level for all loggers in this application (include pyModbusTCP)
logging.basicConfig(format='%(asctime)s - %(name)-20s - %(levelname)-8s - %(message)s', level=logging.INFO)
# set debug level for pyModbusTCP.client to see frame exchanges
logging.getLogger('pyModbusTCP.client').setLevel(logging.DEBUG)
# run a modbus server at localhost:5020
ModbusServer(host='localhost', port=5020, no_block=True).start()
# this message is show
logger.info(f'app startup')
# init modbus client to connect to localhost:5020
c = ModbusClient(port=5020)
# main loop
for i in range(100):
# this message is not not show (global log level is set to INFO)
logger.debug(f'run loop #{i}')
# modbus i/o
c.read_coils(0)
time.sleep(2)
================================================
FILE: examples/client_float.py
================================================
#!/usr/bin/env python3
""" How-to add float support to ModbusClient. """
from pyModbusTCP.client import ModbusClient
from pyModbusTCP.utils import (decode_ieee, encode_ieee, long_list_to_word,
word_list_to_long)
class FloatModbusClient(ModbusClient):
"""A ModbusClient class with float support."""
def read_float(self, address, number=1):
"""Read float(s) with read holding registers."""
reg_l = self.read_holding_registers(address, number * 2)
if reg_l:
return [decode_ieee(f) for f in word_list_to_long(reg_l)]
else:
return None
def write_float(self, address, floats_list):
"""Write float(s) with write multiple registers."""
b32_l = [encode_ieee(f) for f in floats_list]
b16_l = long_list_to_word(b32_l)
return self.write_multiple_registers(address, b16_l)
if __name__ == '__main__':
# init modbus client
c = FloatModbusClient(host='localhost', port=502, auto_open=True)
# write 10.0 at @0
c.write_float(0, [10.0])
# read @0 to 9
float_l = c.read_float(0, 10)
print(float_l)
c.close()
================================================
FILE: examples/client_minimal.py
================================================
#!/usr/bin/env python3
""" Minimal code example. """
from pyModbusTCP.client import ModbusClient
# read 3 coils at @0 on localhost server
print('coils=%s' % ModbusClient().read_coils(0, 3))
================================================
FILE: examples/client_read_coils.py
================================================
#!/usr/bin/env python3
""" Read 10 coils and print result on stdout. """
import time
from pyModbusTCP.client import ModbusClient
# init modbus client
c = ModbusClient(host='localhost', port=502, auto_open=True)
# main read loop
while True:
# read 10 bits (= coils) at address 0, store result in coils list
coils_l = c.read_coils(0, 10)
# if success display registers
if coils_l:
print('coil ad #0 to 9: %s' % coils_l)
else:
print('unable to read coils')
# sleep 2s before next polling
time.sleep(2)
================================================
FILE: examples/client_read_h_registers.py
================================================
#!/usr/bin/env python3
""" Read 10 holding registers and print result on stdout. """
import time
from pyModbusTCP.client import ModbusClient
# init modbus client
c = ModbusClient(auto_open=True)
# main read loop
while True:
# read 10 registers at address 0, store result in regs list
regs_l = c.read_holding_registers(0, 10)
# if success display registers
if regs_l:
print('reg ad #0 to 9: %s' % regs_l)
else:
print('unable to read registers')
# sleep 2s before next polling
time.sleep(2)
================================================
FILE: examples/client_serial_gw.py
================================================
#!/usr/bin/env python3
"""
Modbus RTU to TCP basic gateway (master attached)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Modbus master device -> [Modbus RTU] -> client_serial_gw -> [Modbus/TCP] -> Modbus server
Open /dev/ttyUSB0 at 115200 bauds and relay RTU messages to modbus server for slave address 30.
$ ./client_serial_gw.py /dev/ttyUSB0 --baudrate 115200 --address 30
"""
import argparse
import logging
import struct
# need sudo pip3 install pyserial==3.4
from serial import Serial, serialutil
from pyModbusTCP.client import ModbusClient
from pyModbusTCP.constants import EXP_GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND
from pyModbusTCP.utils import crc16
# some class
class ModbusRTUFrame:
""" Modbus RTU frame container class. """
def __init__(self, raw=b''):
# public
self.raw = raw
def __repr__(self) -> str:
return self.as_hex
@property
def as_hex(self) -> str:
"""Return RAW frame as a hex string."""
return '-'.join(['%02X' % x for x in self.raw])
@property
def pdu(self):
"""Return PDU part of frame."""
return self.raw[1:-2]
@property
def slave_addr(self):
"""Return slave address part of frame."""
return self.raw[0]
@property
def function_code(self):
"""Return function code part of frame."""
return self.raw[1]
@property
def is_set(self):
"""Check if frame is set
:return: True if frame is set
:rtype: bool
"""
return bool(self.raw)
@property
def is_valid(self):
"""Check if frame is valid.
:return: True if frame is valid
:rtype: bool
"""
return len(self.raw) > 4 and crc16(self.raw) == 0
def build(self, raw_pdu, slave_addr):
"""Build a full modbus RTU message from PDU and slave address.
:param raw_pdu: modbus as raw value
:type raw_pdu: bytes
:param slave_addr: address of the slave
:type slave_addr: int
"""
# [address] + PDU
tmp_raw = struct.pack('B', slave_addr) + raw_pdu
# [address] + PDU + [CRC 16]
tmp_raw += struct.pack('<H', crc16(tmp_raw))
self.raw = tmp_raw
class SlaveSerialWorker:
""" A serial worker to manage I/O with RTU master device. """
def __init__(self, port, end_of_frame=0.05):
# public
self.serial_port = port
self.end_of_frame = end_of_frame
self.request = ModbusRTUFrame()
self.response = ModbusRTUFrame()
def handle_request(self):
"""Default PDU request processing here, you must implement it in your app."""
raise RuntimeError('implement this')
def run(self):
"""Serial worker process."""
# flush serial buffer
self.serial_port.reset_input_buffer()
# request loop
while True:
# init a new transaction
self.request = ModbusRTUFrame()
self.response = ModbusRTUFrame()
# receive from serial
# wait for first byte of data
self.serial_port.timeout = None
rx_raw = self.serial_port.read(1)
# if ok, wait for the remaining
if rx_raw:
self.serial_port.timeout = self.end_of_frame
# wait for next bytes of data until end of frame delay
while True:
rx_chunk = self.serial_port.read(256)
if not rx_chunk:
break
else:
rx_raw += rx_chunk
# store the receipt frame
self.request.raw = rx_raw
crc_ok = self.request.is_valid
# log of received items for debugging purposes
logger.debug('Receive: %s (CRC %s)' % (self.request, "OK" if crc_ok else "ERROR"))
# just ignore current frame on CRC error
if not crc_ok:
continue
# relay PDU of request to modbus server
self.handle_request()
# if a response frame is set sent it
if self.response.is_set:
# log sent items for debugging purposes
logger.debug('Send: %s' % self.response)
self.serial_port.write(self.response.raw)
class Serial2ModbusClient:
""" Customize a slave serial worker for map a modbus TCP client. """
def __init__(self, serial_w, mbus_cli, slave_addr=1, allow_bcast=False):
"""Serial2ModbusClient constructor.
:param serial_w: a SlaveSerialWorker instance
:type serial_w: SlaveSerialWorker
:param mbus_cli: a ModbusClient instance
:type mbus_cli: ModbusClient
:param slave_addr: modbus slave address
:type slave_addr: int
:param allow_bcast: allow processing broadcast frames (slave @0)
:type allow_bcast: bool
"""
# public
self.serial_w = serial_w
self.mbus_cli = mbus_cli
self.slave_addr = slave_addr
self.allow_bcast = allow_bcast
# replace serial worker default request handler by Serial2ModbusClient one
self.serial_w.handle_request = self._handle_request
def _handle_request(self):
"""Request handler for SlaveSerialWorker"""
# broadcast/unicast frame ?
if self.serial_w.request.slave_addr == 0 and self.allow_bcast:
# if config allow it, process a broadcast request (=> process it, but don't respond)
self.mbus_cli.custom_request(self.serial_w.request.pdu)
elif self.serial_w.request.slave_addr == self.slave_addr:
# process unicast request
resp_pdu = self.mbus_cli.custom_request(self.serial_w.request.pdu)
# if no error, format a response frame
if resp_pdu:
# regular response from Modbus/TCP client
self.serial_w.response.build(raw_pdu=resp_pdu, slave_addr=self.serial_w.request.slave_addr)
else:
# exception response
exp_pdu = struct.pack('BB', self.serial_w.request.function_code + 0x80,
EXP_GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND)
self.serial_w.response.build(raw_pdu=exp_pdu, slave_addr=self.serial_w.request.slave_addr)
def run(self):
"""Start serial processing."""
self.serial_w.run()
if __name__ == '__main__':
# parse args
parser = argparse.ArgumentParser()
parser.add_argument('serial_device', type=str, help='serial device (like /dev/ttyUSB0)')
parser.add_argument('-d', '--debug', action='store_true', help='debug mode')
parser.add_argument('-a', '--address', type=int, default=1, help='slave address (default is 1)')
parser.add_argument('--allow-broadcast', action='store_true', help='serial allow broadcast frame (to address 0)')
parser.add_argument('-b', '--baudrate', type=int, default=9600, help='serial rate (default is 9600)')
parser.add_argument('-e', '--eof', type=float, default=0.05, help='serial end of frame delay in s (default: 0.05)')
parser.add_argument('-H', '--host', type=str, default='localhost', help='server host (default: localhost)')
parser.add_argument('-p', '--port', type=int, default=502, help='server TCP port (default: 502)')
parser.add_argument('-t', '--timeout', type=float, default=1.0, help='server timeout delay in s (default: 1.0)')
args = parser.parse_args()
# init logging
logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
logger = logging.getLogger(__name__)
try:
# init serial port
logger.info('Open serial port %s at %d bauds (eof = %.2fs)', args.serial_device, args.baudrate, args.eof)
serial_port = Serial(port=args.serial_device, baudrate=args.baudrate)
# start modbus client as request relay
logger.info('Connect to modbus server at %s:%d (timeout = %.2fs)', args.host, args.port, args.timeout)
mbus_cli = ModbusClient(host=args.host, port=args.port, unit_id=1, timeout=args.timeout)
# init serial worker
serial_worker = SlaveSerialWorker(serial_port, end_of_frame=args.eof)
# start Serial2ModbusClient
logger.info('Start serial worker (slave address = %d)' % args.address)
Serial2ModbusClient(mbus_cli=mbus_cli, serial_w=serial_worker,
slave_addr=args.address, allow_bcast=args.allow_broadcast).run()
except serialutil.SerialException as e:
logger.critical('Serial device error: %r', e)
exit(1)
================================================
FILE: examples/client_thread.py
================================================
#!/usr/bin/env python3
"""
modbus polling thread
~~~~~~~~~~~~~~~~~~~~~
Start a thread for polling a set of registers, display result on console.
Exit with ctrl+c.
"""
import time
from threading import Lock, Thread
from pyModbusTCP.client import ModbusClient
SERVER_HOST = "localhost"
SERVER_PORT = 502
# set global
regs = []
# init a thread lock
regs_lock = Lock()
def polling_thread():
"""Modbus polling thread."""
global regs, regs_lock
c = ModbusClient(host=SERVER_HOST, port=SERVER_PORT, auto_open=True)
# polling loop
while True:
# do modbus reading on socket
reg_list = c.read_holding_registers(0, 10)
# if read is ok, store result in regs (with thread lock)
if reg_list:
with regs_lock:
regs = list(reg_list)
# 1s before next polling
time.sleep(1)
# start polling thread
tp = Thread(target=polling_thread)
# set daemon: polling thread will exit if main thread exit
tp.daemon = True
tp.start()
# display loop (in main thread)
while True:
# print regs list (with thread lock synchronization)
with regs_lock:
print(regs)
# 1s before next print
time.sleep(1)
================================================
FILE: examples/client_write_coils.py
================================================
#!/usr/bin/env python3
"""Write 4 coils to True, wait 2s, write False and redo it."""
import time
from pyModbusTCP.client import ModbusClient
# init
c = ModbusClient(host='localhost', port=502, auto_open=True)
bit = True
# main loop
while True:
# write 4 bits in modbus address 0 to 3
print('write bits')
print('----------\n')
for ad in range(4):
is_ok = c.write_single_coil(ad, bit)
if is_ok:
print('coil #%s: write to %s' % (ad, bit))
else:
print('coil #%s: unable to write %s' % (ad, bit))
time.sleep(0.5)
print('')
time.sleep(1)
# read 4 bits in modbus address 0 to 3
print('read bits')
print('---------\n')
bits = c.read_coils(0, 4)
if bits:
print('coils #0 to 3: %s' % bits)
else:
print('coils #0 to 3: unable to read')
# toggle
bit = not bit
# sleep 2s before next polling
print('')
time.sleep(2)
================================================
FILE: examples/server.py
================================================
#!/usr/bin/env python3
"""
Modbus/TCP server
~~~~~~~~~~~~~~~~~
Run this as root to listen on TCP privileged ports (<= 1024).
Add "--host 0.0.0.0" to listen on all available IPv4 addresses of the host.
$ sudo ./server.py --host 0.0.0.0
"""
import argparse
import logging
from pyModbusTCP.server import ModbusServer
# init logging
logging.basicConfig()
# parse args
parser = argparse.ArgumentParser()
parser.add_argument('-H', '--host', type=str, default='localhost', help='Host (default: localhost)')
parser.add_argument('-p', '--port', type=int, default=502, help='TCP port (default: 502)')
parser.add_argument('-d', '--debug', action='store_true', help='set debug mode')
args = parser.parse_args()
# logging setup
if args.debug:
logging.getLogger('pyModbusTCP.server').setLevel(logging.DEBUG)
# start modbus server
server = ModbusServer(host=args.host, port=args.port)
server.start()
================================================
FILE: examples/server_allow.py
================================================
#!/usr/bin/env python3
"""
An example of Modbus/TCP server which allow modbus read and/or write only from
specific IPs.
Run this as root to listen on TCP privileged ports (<= 1024).
"""
import argparse
from pyModbusTCP.constants import EXP_ILLEGAL_FUNCTION
from pyModbusTCP.server import DataHandler, ModbusServer
# some const
ALLOW_R_L = ['127.0.0.1', '192.168.0.10']
ALLOW_W_L = ['127.0.0.1']
# a custom data handler with IPs filter
class MyDataHandler(DataHandler):
def read_coils(self, address, count, srv_info):
if srv_info.client.address in ALLOW_R_L:
return super().read_coils(address, count, srv_info)
else:
return DataHandler.Return(exp_code=EXP_ILLEGAL_FUNCTION)
def read_d_inputs(self, address, count, srv_info):
if srv_info.client.address in ALLOW_R_L:
return super().read_d_inputs(address, count, srv_info)
else:
return DataHandler.Return(exp_code=EXP_ILLEGAL_FUNCTION)
def read_h_regs(self, address, count, srv_info):
if srv_info.client.address in ALLOW_R_L:
return super().read_h_regs(address, count, srv_info)
else:
return DataHandler.Return(exp_code=EXP_ILLEGAL_FUNCTION)
def read_i_regs(self, address, count, srv_info):
if srv_info.client.address in ALLOW_R_L:
return super().read_i_regs(address, count, srv_info)
else:
return DataHandler.Return(exp_code=EXP_ILLEGAL_FUNCTION)
def write_coils(self, address, bits_l, srv_info):
if srv_info.client.address in ALLOW_W_L:
return super().write_coils(address, bits_l, srv_info)
else:
return DataHandler.Return(exp_code=EXP_ILLEGAL_FUNCTION)
def write_h_regs(self, address, words_l, srv_info):
if srv_info.client.address in ALLOW_W_L:
return super().write_h_regs(address, words_l, srv_info)
else:
return DataHandler.Return(exp_code=EXP_ILLEGAL_FUNCTION)
if __name__ == '__main__':
# parse args
parser = argparse.ArgumentParser()
parser.add_argument('-H', '--host', type=str, default='localhost', help='Host (default: localhost)')
parser.add_argument('-p', '--port', type=int, default=502, help='TCP port (default: 502)')
args = parser.parse_args()
# init modbus server and start it
server = ModbusServer(host=args.host, port=args.port, data_hdl=MyDataHandler())
server.start()
================================================
FILE: examples/server_change_log.py
================================================
#!/usr/bin/env python3
"""
An example of Modbus/TCP server with a change logger.
Run this as root to listen on TCP privileged ports (<= 1024).
"""
import argparse
import logging
from pyModbusTCP.server import DataBank, ModbusServer
class MyDataBank(DataBank):
"""A custom ModbusServerDataBank for override on_xxx_change methods."""
def on_coils_change(self, address, from_value, to_value, srv_info):
"""Call by server when change occur on coils space."""
msg = 'change in coil space [{0!r:^5} > {1!r:^5}] at @ 0x{2:04X} from ip: {3:<15}'
msg = msg.format(from_value, to_value, address, srv_info.client.address)
logging.info(msg)
def on_holding_registers_change(self, address, from_value, to_value, srv_info):
"""Call by server when change occur on holding registers space."""
msg = 'change in hreg space [{0!r:^5} > {1!r:^5}] at @ 0x{2:04X} from ip: {3:<15}'
msg = msg.format(from_value, to_value, address, srv_info.client.address)
logging.info(msg)
if __name__ == '__main__':
# parse args
parser = argparse.ArgumentParser()
parser.add_argument('-H', '--host', type=str, default='localhost', help='Host (default: localhost)')
parser.add_argument('-p', '--port', type=int, default=502, help='TCP port (default: 502)')
args = parser.parse_args()
# logging setup
logging.basicConfig(format='%(asctime)s %(message)s', level=logging.INFO)
# init modbus server and start it
server = ModbusServer(host=args.host, port=args.port, data_bank=MyDataBank())
server.start()
================================================
FILE: examples/server_schedule.py
================================================
#!/usr/bin/env python3
"""
Modbus/TCP server with start/stop schedule
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Run this as root to listen on TCP privileged ports (<= 1024).
Default Modbus/TCP port is 502, so we prefix call with sudo. With argument
"--host 0.0.0.0", server listen on all IPv4 of the host. Instead of just
open tcp/502 on local interface.
$ sudo ./server_schedule.py --host 0.0.0.0
"""
import argparse
import time
# need https://github.com/dbader/schedule
import schedule
from pyModbusTCP.server import ModbusServer
def alive_word_job():
"""Update holding register @0 with day second (since 00:00).
Job called every 10s by scheduler.
"""
server.data_bank.set_holding_registers(0, [int(time.time()) % (24*3600) // 10])
# parse args
parser = argparse.ArgumentParser()
parser.add_argument('-H', '--host', type=str, default='localhost', help='Host (default: localhost)')
parser.add_argument('-p', '--port', type=int, default=502, help='TCP port (default: 502)')
args = parser.parse_args()
# init modbus server and start it
server = ModbusServer(host=args.host, port=args.port, no_block=True)
server.start()
# init scheduler
# schedule a daily downtime (from 18:00 to 06:00)
schedule.every().day.at('18:00').do(server.stop)
schedule.every().day.at('06:00').do(server.start)
# update life word at @0
schedule.every(10).seconds.do(alive_word_job)
# main loop
while True:
schedule.run_pending()
time.sleep(1)
================================================
FILE: examples/server_serial_gw.py
================================================
#!/usr/bin/env python3
"""
Modbus/TCP basic gateway (RTU slave(s) attached)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[pyModbusTCP server] -> [ModbusSerialWorker] -> [serial RTU devices]
Run this as root to listen on TCP privileged ports (<= 1024).
Open /dev/ttyUSB0 at 115200 bauds and relay it RTU messages to slave(s).
$ sudo ./server_serial_gw.py --baudrate 115200 /dev/ttyUSB0
"""
import argparse
import logging
import queue
import struct
from queue import Queue
from threading import Event
# need sudo pip install pyserial==3.4
from serial import Serial, serialutil
from pyModbusTCP.constants import (EXP_GATEWAY_PATH_UNAVAILABLE,
EXP_GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND)
from pyModbusTCP.server import ModbusServer
from pyModbusTCP.utils import crc16
# some class
class ModbusRTUFrame:
""" Modbus RTU frame container class. """
def __init__(self, raw=b''):
# public
self.raw = raw
@property
def pdu(self):
"""Return PDU part of frame."""
return self.raw[1:-2]
@property
def slave_address(self):
"""Return slave address part of frame."""
return self.raw[0]
@property
def function_code(self):
"""Return function code part of frame."""
return self.raw[1]
@property
def is_valid(self):
"""Check if frame is valid.
:return: True if frame is valid
:rtype: bool
"""
return len(self.raw) > 4 and crc16(self.raw) == 0
def build(self, raw_pdu, slave_ad):
"""Build a full modbus RTU message from PDU and slave address.
:param raw_pdu: modbus as raw value
:type raw_pdu: bytes
:param slave_ad: address of the slave
:type slave_ad: int
"""
# [address] + PDU
tmp_raw = struct.pack('B', slave_ad) + raw_pdu
# [address] + PDU + [CRC 16]
tmp_raw += struct.pack('<H', crc16(tmp_raw))
self.raw = tmp_raw
class RtuQuery:
""" Request container to deal with modbus serial worker. """
def __init__(self):
self.completed = Event()
self.request = ModbusRTUFrame()
self.response = ModbusRTUFrame()
class ModbusSerialWorker:
""" A serial worker to manage I/O with RTU devices. """
def __init__(self, port, timeout=1.0, end_of_frame=0.05):
# public
self.serial_port = port
self.timeout = timeout
self.end_of_frame = end_of_frame
# internal request queue
# accept 5 simultaneous requests before overloaded exception is return
self.rtu_queries_q = Queue(maxsize=5)
def loop(self):
"""Serial worker main loop."""
while True:
# get next exchange from queue
rtu_query = self.rtu_queries_q.get()
# send to serial
self.serial_port.reset_input_buffer()
self.serial_port.write(rtu_query.request.raw)
# receive from serial
# wait for first byte of data until timeout delay
self.serial_port.timeout = self.timeout
rx_raw = self.serial_port.read(1)
# if ok, wait for the remaining
if rx_raw:
self.serial_port.timeout = self.end_of_frame
# wait for next bytes of data until end of frame delay
while True:
rx_chunk = self.serial_port.read(256)
if not rx_chunk:
break
else:
rx_raw += rx_chunk
rtu_query.response.raw = rx_raw
# mark all as done
rtu_query.completed.set()
self.rtu_queries_q.task_done()
def srv_engine_entry(self, session_data):
"""Server engine entry point (pass request to serial worker queries queue).
:param session_data: server session data
:type session_data: ModbusServer.SessionData
"""
# init a serial exchange from session data
rtu_query = RtuQuery()
rtu_query.request.build(raw_pdu=session_data.request.pdu.raw,
slave_ad=session_data.request.mbap.unit_id)
try:
# add a request in the serial worker queue, can raise queue.Full
self.rtu_queries_q.put(rtu_query, block=False)
# wait result
rtu_query.completed.wait()
# check receive frame status
if rtu_query.response.is_valid:
session_data.response.pdu.raw = rtu_query.response.pdu
return
# except status for slave failed to respond
exp_status = EXP_GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND
except queue.Full:
# except status for overloaded gateway
exp_status = EXP_GATEWAY_PATH_UNAVAILABLE
# return modbus exception
func_code = rtu_query.request.function_code
session_data.response.pdu.build_except(func_code=func_code, exp_status=exp_status)
if __name__ == '__main__':
# parse args
parser = argparse.ArgumentParser()
parser.add_argument('device', type=str, help='serial device (like /dev/ttyUSB0)')
parser.add_argument('-H', '--host', type=str, default='localhost', help='host (default: localhost)')
parser.add_argument('-p', '--port', type=int, default=502, help='TCP port (default: 502)')
parser.add_argument('-b', '--baudrate', type=int, default=9600, help='serial rate (default is 9600)')
parser.add_argument('-t', '--timeout', type=float, default=1.0, help='timeout delay (default is 1.0 s)')
parser.add_argument('-e', '--eof', type=float, default=0.05, help='end of frame delay (default is 0.05 s)')
parser.add_argument('-d', '--debug', action='store_true', help='set debug mode')
args = parser.parse_args()
# init logging
logging.basicConfig(level=logging.DEBUG if args.debug else None)
logger = logging.getLogger(__name__)
try:
# init serial port
logger.debug('Open serial port %s at %d bauds', args.device, args.baudrate)
serial_port = Serial(port=args.device, baudrate=args.baudrate)
# init serial worker
serial_worker = ModbusSerialWorker(serial_port, args.timeout, args.eof)
# start modbus server with custom engine
logger.debug('Start modbus server (%s, %d)', args.host, args.port)
srv = ModbusServer(host=args.host, port=args.port,
no_block=True, ext_engine=serial_worker.srv_engine_entry)
srv.start()
# start serial worker loop
logger.debug('Start serial worker')
serial_worker.loop()
except serialutil.SerialException as e:
logger.critical('Serial device error: %r', e)
exit(1)
except ModbusServer.Error as e:
logger.critical('Modbus server error: %r', e)
exit(2)
================================================
FILE: examples/server_virtual_data.py
================================================
#!/usr/bin/env python3
"""
Modbus/TCP server with virtual data
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Map the system date and time to @ 0 to 5 on the "holding registers" space.
Only the reading of these registers in this address space is authorized. All
other requests return an illegal data address except.
Run this as root to listen on TCP priviliged ports (<= 1024).
"""
import argparse
from datetime import datetime
from pyModbusTCP.server import DataBank, ModbusServer
class MyDataBank(DataBank):
"""A custom ModbusServerDataBank for override get_holding_registers method."""
def __init__(self):
# turn off allocation of memory for standard modbus object types
# only "holding registers" space will be replaced by dynamic build values.
super().__init__(virtual_mode=True)
def get_holding_registers(self, address, number=1, srv_info=None):
"""Get virtual holding registers."""
# populate virtual registers dict with current datetime values
now = datetime.now()
v_regs_d = {0: now.day, 1: now.month, 2: now.year,
3: now.hour, 4: now.minute, 5: now.second}
# build a list of virtual regs to return to server data handler
# return None if any of virtual registers is missing
try:
return [v_regs_d[a] for a in range(address, address+number)]
except KeyError:
return
if __name__ == '__main__':
# parse args
parser = argparse.ArgumentParser()
parser.add_argument('-H', '--host', type=str, default='localhost', help='Host (default: localhost)')
parser.add_argument('-p', '--port', type=int, default=502, help='TCP port (default: 502)')
args = parser.parse_args()
# init modbus server and start it
server = ModbusServer(host=args.host, port=args.port, data_bank=MyDataBank())
server.start()
================================================
FILE: pyModbusTCP/__init__.py
================================================
# Python package: Client and Server for ModBus/TCP
# Website: https://github.com/sourceperl/pyModbusTCP
# License: MIT (http://http://opensource.org/licenses/mit-license.php)
# Description: Client/Server ModBus/TCP
# Support functions 3 and 16 (class 0)
# 1,2,4,5,6 (Class 1)
# 15,23,43
import logging
from .constants import VERSION
__title__ = 'pyModbusTCP'
__description__ = 'A simple Modbus/TCP library for Python.'
__url__ = 'https://github.com/sourceperl/pyModbusTCP'
__version__ = VERSION
__license__ = 'MIT'
logger = logging.getLogger(__name__)
================================================
FILE: pyModbusTCP/client.py
================================================
""" pyModbusTCP Client """
import logging
import random
import socket
import struct
from binascii import hexlify
from dataclasses import dataclass, field
from socket import AF_UNSPEC, SOCK_STREAM
from typing import Dict
from .constants import (ENCAPSULATED_INTERFACE_TRANSPORT, EXP_DETAILS,
EXP_NONE, EXP_TXT, MB_CONNECT_ERR, MB_ERR_TXT,
MB_EXCEPT_ERR, MB_NO_ERR, MB_RECV_ERR, MB_SEND_ERR,
MB_SOCK_CLOSE_ERR, MB_TIMEOUT_ERR,
MEI_TYPE_READ_DEVICE_ID, READ_COILS,
READ_DISCRETE_INPUTS, READ_HOLDING_REGISTERS,
READ_INPUT_REGISTERS, VERSION, WRITE_MULTIPLE_COILS,
WRITE_MULTIPLE_REGISTERS,
WRITE_READ_MULTIPLE_REGISTERS, WRITE_SINGLE_COIL,
WRITE_SINGLE_REGISTER)
from .utils import byte_length, set_bit, valid_host
# add a logger for pyModbusTCP.client
logger = logging.getLogger(__name__)
@dataclass
class DeviceIdentificationResponse:
"""Modbus TCP client function read_device_identification() response struct.
:param conformity_level: this represents supported access and object type
:type conformity_level: int
:param more_follows: for stream request can be set to 0xff if other objects are available (0x00 in other cases)
:type more_follows: int
:param next_object_id: the next object id to be asked by following transaction
:type next_object_id: int
:param objects_by_id: a dictionary with requested object (dict keys are object id as int)
:type objects_by_id: dict
"""
conformity_level: int = 0
more_follows: int = 0
next_object_id: int = 0
objects_by_id: Dict[int, bytes] = field(default_factory=lambda: {})
@property
def vendor_name(self):
return self.objects_by_id.get(0x00)
@property
def product_code(self):
return self.objects_by_id.get(0x01)
@property
def major_minor_revision(self):
return self.objects_by_id.get(0x02)
@property
def vendor_url(self):
return self.objects_by_id.get(0x03)
@property
def product_name(self):
return self.objects_by_id.get(0x04)
@property
def model_name(self):
return self.objects_by_id.get(0x05)
@property
def user_application_name(self):
return self.objects_by_id.get(0x06)
class ModbusClient:
"""Modbus TCP client."""
class _InternalError(Exception):
pass
class _NetworkError(_InternalError):
def __init__(self, code, message):
self.code = code
self.message = message
class _ModbusExcept(_InternalError):
def __init__(self, code):
self.code = code
def __init__(self, host='localhost', port=502, unit_id=1, timeout=30.0, auto_open=True, auto_close=False):
"""Constructor.
:param host: hostname or IPv4/IPv6 address server address
:type host: str
:param port: TCP port number
:type port: int
:param unit_id: unit ID
:type unit_id: int
:param timeout: socket timeout in seconds
:type timeout: float
:param auto_open: auto TCP connect
:type auto_open: bool
:param auto_close: auto TCP close)
:type auto_close: bool
:return: Object ModbusClient
:rtype: ModbusClient
"""
# private
# internal variables
self._host = None
self._port = None
self._unit_id = None
self._timeout = None
self._auto_open = None
self._auto_close = None
self._sock = socket.socket()
self._transaction_id = 0 # MBAP transaction ID
self._version = VERSION # this package version number
self._last_error = MB_NO_ERR # last error code
self._last_except = EXP_NONE # last except code
# public
# constructor arguments: validate them with property setters
self.host = host
self.port = port
self.unit_id = unit_id
self.timeout = timeout
self.auto_open = auto_open
self.auto_close = auto_close
def __repr__(self):
r_str = 'ModbusClient(host=\'%s\', port=%d, unit_id=%d, timeout=%.2f, auto_open=%s, auto_close=%s)'
r_str %= (self.host, self.port, self.unit_id, self.timeout, self.auto_open, self.auto_close)
return r_str
def __del__(self):
self.close()
@property
def version(self):
"""Return the current package version as a str."""
return self._version
@property
def last_error(self):
"""Last error code."""
return self._last_error
@property
def last_error_as_txt(self):
"""Human-readable text that describe last error."""
return MB_ERR_TXT.get(self._last_error, 'unknown error')
@property
def last_except(self):
"""Return the last modbus exception code."""
return self._last_except
@property
def last_except_as_txt(self):
"""Short human-readable text that describe last modbus exception."""
default_str = 'unreferenced exception 0x%X' % self._last_except
return EXP_TXT.get(self._last_except, default_str)
@property
def last_except_as_full_txt(self):
"""Verbose human-readable text that describe last modbus exception."""
default_str = 'unreferenced exception 0x%X' % self._last_except
return EXP_DETAILS.get(self._last_except, default_str)
@property
def host(self):
"""Get or set the server to connect to.
This can be any string with a valid IPv4 / IPv6 address or hostname.
Setting host to a new value will close the current socket.
"""
return self._host
@host.setter
def host(self, value):
# check type
if type(value) is not str:
raise TypeError('host must be a str')
# check value
if valid_host(value):
if self._host != value:
self.close()
self._host = value
return
# can't be set
raise ValueError('host can\'t be set (not a valid IP address or hostname)')
@property
def port(self):
"""Get or set the current TCP port (default is 502).
Setting port to a new value will close the current socket.
"""
return self._port
@port.setter
def port(self, value):
# check type
if type(value) is not int:
raise TypeError('port must be an int')
# check validity
if 0 < value < 65536:
if self._port != value:
self.close()
self._port = value
return
# can't be set
raise ValueError('port can\'t be set (valid if 0 < port < 65536)')
@property
def unit_id(self):
"""Get or set the modbus unit identifier (default is 1).
Any int from 0 to 255 is valid.
"""
return self._unit_id
@unit_id.setter
def unit_id(self, value):
# check type
if type(value) is not int:
raise TypeError('unit_id must be an int')
# check validity
if 0 <= value <= 255:
self._unit_id = value
return
# can't be set
raise ValueError('unit_id can\'t be set (valid from 0 to 255)')
@property
def timeout(self):
"""Get or set requests timeout (default is 30 seconds).
The argument may be a floating point number for sub-second precision.
Setting timeout to a new value will close the current socket.
"""
return self._timeout
@timeout.setter
def timeout(self, value):
# enforce type
value = float(value)
# check validity
if 0 < value < 3600:
if self._timeout != value:
self.close()
self._timeout = value
return
# can't be set
raise ValueError('timeout can\'t be set (valid between 0 and 3600)')
@property
def auto_open(self):
"""Get or set automatic TCP connect mode (True = turn on)."""
return self._auto_open
@auto_open.setter
def auto_open(self, value):
# enforce type
self._auto_open = bool(value)
@property
def auto_close(self):
"""Get or set automatic TCP close after each request mode (True = turn on)."""
return self._auto_close
@auto_close.setter
def auto_close(self, value):
# enforce type
self._auto_close = bool(value)
@property
def is_open(self):
"""Get current status of the TCP connection (True = open)."""
return self._sock.fileno() > 0
def open(self):
"""Connect to modbus server (open TCP connection).
:returns: connect status (True on success)
:rtype: bool
"""
try:
self._open()
return True
except ModbusClient._NetworkError as e:
self._req_except_handler(e)
return False
def _open(self):
"""Connect to modbus server (open TCP connection)."""
# open an already open socket -> reset it
if self.is_open:
self.close()
# init socket and connect
# list available sockets on the target host/port
# AF_xxx : AF_INET -> IPv4, AF_INET6 -> IPv6,
# AF_UNSPEC -> IPv6 (priority on some system) or 4
# list available socket on target host
for res in socket.getaddrinfo(self.host, self.port, AF_UNSPEC, SOCK_STREAM):
af, sock_type, proto, canon_name, sa = res
try:
self._sock = socket.socket(af, sock_type, proto)
except socket.error:
continue
try:
self._sock.settimeout(self.timeout)
self._sock.connect(sa)
except socket.error:
self._sock.close()
continue
break
# check connect status
if not self.is_open:
raise ModbusClient._NetworkError(MB_CONNECT_ERR, 'connection refused')
def close(self):
"""Close current TCP connection."""
self._sock.close()
def custom_request(self, pdu):
"""Send a custom modbus request.
:param pdu: a modbus PDU (protocol data unit)
:type pdu: bytes
:returns: modbus frame PDU or None if error
:rtype: bytes or None
"""
# make request
try:
return self._req_pdu(pdu)
# handle errors during request
except ModbusClient._InternalError as e:
self._req_except_handler(e)
return None
def read_coils(self, bit_addr, bit_nb=1):
"""Modbus function READ_COILS (0x01).
:param bit_addr: bit address (0 to 65535)
:type bit_addr: int
:param bit_nb: number of bits to read (1 to 2000)
:type bit_nb: int
:returns: bits list or None if error
:rtype: list of bool or None
"""
# check params
if not 0 <= int(bit_addr) <= 0xffff:
raise ValueError('bit_addr out of range (valid from 0 to 65535)')
if not 1 <= int(bit_nb) <= 2000:
raise ValueError('bit_nb out of range (valid from 1 to 2000)')
if int(bit_addr) + int(bit_nb) > 0x10000:
raise ValueError('read after end of modbus address space')
# make request
try:
tx_pdu = struct.pack('>BHH', READ_COILS, bit_addr, bit_nb)
rx_pdu = self._req_pdu(tx_pdu=tx_pdu, rx_min_len=3)
# field "byte count" from PDU
byte_count = rx_pdu[1]
# coils PDU part
rx_pdu_coils = rx_pdu[2:]
# check rx_byte_count: match nb of bits request and check buffer size
if byte_count < byte_length(bit_nb) or byte_count != len(rx_pdu_coils):
raise ModbusClient._NetworkError(MB_RECV_ERR, 'rx byte count mismatch')
# allocate coils list to return
ret_coils = [False] * bit_nb
# populate it with coils value from the rx PDU
for i in range(bit_nb):
ret_coils[i] = bool((rx_pdu_coils[i // 8] >> i % 8) & 0x01)
# return read coils
return ret_coils
# handle error during request
except ModbusClient._InternalError as e:
self._req_except_handler(e)
return None
def read_discrete_inputs(self, bit_addr, bit_nb=1):
"""Modbus function READ_DISCRETE_INPUTS (0x02).
:param bit_addr: bit address (0 to 65535)
:type bit_addr: int
:param bit_nb: number of bits to read (1 to 2000)
:type bit_nb: int
:returns: bits list or None if error
:rtype: list of bool or None
"""
# check params
if not 0 <= int(bit_addr) <= 0xffff:
raise ValueError('bit_addr out of range (valid from 0 to 65535)')
if not 1 <= int(bit_nb) <= 2000:
raise ValueError('bit_nb out of range (valid from 1 to 2000)')
if int(bit_addr) + int(bit_nb) > 0x10000:
raise ValueError('read after end of modbus address space')
# make request
try:
tx_pdu = struct.pack('>BHH', READ_DISCRETE_INPUTS, bit_addr, bit_nb)
rx_pdu = self._req_pdu(tx_pdu=tx_pdu, rx_min_len=3)
# extract field "byte count"
byte_count = rx_pdu[1]
# frame with bits value -> bits[] list
rx_pdu_d_inputs = rx_pdu[2:]
# check rx_byte_count: match nb of bits request and check buffer size
if byte_count < byte_length(bit_nb) or byte_count != len(rx_pdu_d_inputs):
raise ModbusClient._NetworkError(MB_RECV_ERR, 'rx byte count mismatch')
# allocate a bit_nb size list
bits = [False] * bit_nb
# fill bits list with bit items
for i in range(bit_nb):
bits[i] = bool((rx_pdu_d_inputs[i // 8] >> i % 8) & 0x01)
# return bits list
return bits
# handle error during request
except ModbusClient._InternalError as e:
self._req_except_handler(e)
return None
def read_holding_registers(self, reg_addr, reg_nb=1):
"""Modbus function READ_HOLDING_REGISTERS (0x03).
:param reg_addr: register address (0 to 65535)
:type reg_addr: int
:param reg_nb: number of registers to read (1 to 125)
:type reg_nb: int
:returns: registers list or None if fail
:rtype: list of int or None
"""
# check params
if not 0 <= int(reg_addr) <= 0xffff:
raise ValueError('reg_addr out of range (valid from 0 to 65535)')
if not 1 <= int(reg_nb) <= 125:
raise ValueError('reg_nb out of range (valid from 1 to 125)')
if int(reg_addr) + int(reg_nb) > 0x10000:
raise ValueError('read after end of modbus address space')
# make request
try:
tx_pdu = struct.pack('>BHH', READ_HOLDING_REGISTERS, reg_addr, reg_nb)
rx_pdu = self._req_pdu(tx_pdu=tx_pdu, rx_min_len=3)
# extract field "byte count"
byte_count = rx_pdu[1]
# frame with regs value
f_regs = rx_pdu[2:]
# check rx_byte_count: buffer size must be consistent and have at least the requested number of registers
if byte_count < 2 * reg_nb or byte_count != len(f_regs):
raise ModbusClient._NetworkError(MB_RECV_ERR, 'rx byte count mismatch')
# allocate a reg_nb size list
registers = [0] * reg_nb
# fill registers list with register items
for i in range(reg_nb):
registers[i] = struct.unpack('>H', f_regs[i * 2:i * 2 + 2])[0]
# return registers list
return registers
# handle error during request
except ModbusClient._InternalError as e:
self._req_except_handler(e)
return None
def read_input_registers(self, reg_addr, reg_nb=1):
"""Modbus function READ_INPUT_REGISTERS (0x04).
:param reg_addr: register address (0 to 65535)
:type reg_addr: int
:param reg_nb: number of registers to read (1 to 125)
:type reg_nb: int
:returns: registers list or None if fail
:rtype: list of int or None
"""
# check params
if not 0 <= int(reg_addr) <= 0xffff:
raise ValueError('reg_addr out of range (valid from 0 to 65535)')
if not 1 <= int(reg_nb) <= 125:
raise ValueError('reg_nb out of range (valid from 1 to 125)')
if int(reg_addr) + int(reg_nb) > 0x10000:
raise ValueError('read after end of modbus address space')
# make request
try:
tx_pdu = struct.pack('>BHH', READ_INPUT_REGISTERS, reg_addr, reg_nb)
rx_pdu = self._req_pdu(tx_pdu=tx_pdu, rx_min_len=3)
# extract field "byte count"
byte_count = rx_pdu[1]
# frame with regs value
f_regs = rx_pdu[2:]
# check rx_byte_count: buffer size must be consistent and have at least the requested number of registers
if byte_count < 2 * reg_nb or byte_count != len(f_regs):
raise ModbusClient._NetworkError(MB_RECV_ERR, 'rx byte count mismatch')
# allocate a reg_nb size list
registers = [0] * reg_nb
# fill registers list with register items
for i in range(reg_nb):
registers[i] = struct.unpack('>H', f_regs[i * 2:i * 2 + 2])[0]
# return registers list
return registers
# handle error during request
except ModbusClient._InternalError as e:
self._req_except_handler(e)
return None
def read_device_identification(self, read_code=1, object_id=0):
"""Modbus function Read Device Identification (0x2B/0x0E).
:param read_code: read device id code, 1 to 3 for respectively: basic, regular and extended stream access,
4 for one specific identification object individual access (default is 1)
:type read_code: int
:param object_id: object id of the first object to obtain (default is 0)
:type object_id: int
:returns: a DeviceIdentificationResponse instance with the data or None if the requests fails
:rtype: DeviceIdentificationResponse or None
"""
# check params
if not 1 <= int(read_code) <= 4:
raise ValueError('read_code out of range (valid from 1 to 4)')
if not 0 <= int(object_id) <= 0xff:
raise ValueError('object_id out of range (valid from 0 to 255)')
# make request
try:
tx_pdu = struct.pack('BBBB', ENCAPSULATED_INTERFACE_TRANSPORT,
MEI_TYPE_READ_DEVICE_ID, read_code, object_id)
rx_pdu = self._req_pdu(tx_pdu=tx_pdu, rx_min_len=7)
# init response object for populate it
response = DeviceIdentificationResponse()
# extract fields
# field "conformity level"
response.conformity_level = rx_pdu[3]
# field "more follows"
response.more_follows = rx_pdu[4]
# field "next object id"
response.next_object_id = rx_pdu[5]
# field "number of objects"
nb_of_objs = rx_pdu[6]
# decode [[obj_id, obj_len, obj_value],...]
pdu_offset = 7
for _ in range(nb_of_objs):
# parse object PDU bytes
try:
obj_id = rx_pdu[pdu_offset]
obj_len = rx_pdu[pdu_offset+1]
obj_value = rx_pdu[pdu_offset+2:pdu_offset+2+obj_len]
except IndexError:
raise ModbusClient._NetworkError(MB_RECV_ERR, 'rx byte count mismatch')
if obj_len != len(obj_value):
raise ModbusClient._NetworkError(MB_RECV_ERR, 'rx byte count mismatch')
# set offset to next object
pdu_offset += 2 + obj_len
# add result to request list
response.objects_by_id[obj_id] = obj_value
return response
# handle error during request
except ModbusClient._InternalError as e:
self._req_except_handler(e)
return None
def write_single_coil(self, bit_addr, bit_value):
"""Modbus function WRITE_SINGLE_COIL (0x05).
:param bit_addr: bit address (0 to 65535)
:type bit_addr: int
:param bit_value: bit value to write
:type bit_value: bool
:returns: True if write ok
:rtype: bool
"""
# check params
if not 0 <= int(bit_addr) <= 0xffff:
raise ValueError('bit_addr out of range (valid from 0 to 65535)')
# make request
try:
# format "bit value" field for PDU
bit_value_raw = (0x0000, 0xff00)[bool(bit_value)]
# make a request
tx_pdu = struct.pack('>BHH', WRITE_SINGLE_COIL, bit_addr, bit_value_raw)
rx_pdu = self._req_pdu(tx_pdu=tx_pdu, rx_min_len=5)
# decode reply
resp_coil_addr, resp_coil_value = struct.unpack('>HH', rx_pdu[1:5])
# check server reply
if (resp_coil_addr != bit_addr) or (resp_coil_value != bit_value_raw):
raise ModbusClient._NetworkError(MB_RECV_ERR, 'server reply does not match the request')
return True
# handle error during request
except ModbusClient._InternalError as e:
self._req_except_handler(e)
return False
def write_single_register(self, reg_addr, reg_value):
"""Modbus function WRITE_SINGLE_REGISTER (0x06).
:param reg_addr: register address (0 to 65535)
:type reg_addr: int
:param reg_value: register value to write
:type reg_value: int
:returns: True if write ok
:rtype: bool
"""
# check params
if not 0 <= int(reg_addr) <= 0xffff:
raise ValueError('reg_addr out of range (valid from 0 to 65535)')
if not 0 <= int(reg_value) <= 0xffff:
raise ValueError('reg_value out of range (valid from 0 to 65535)')
# make request
try:
# make a request
tx_pdu = struct.pack('>BHH', WRITE_SINGLE_REGISTER, reg_addr, reg_value)
rx_pdu = self._req_pdu(tx_pdu=tx_pdu, rx_min_len=5)
# decode reply
resp_reg_addr, resp_reg_value = struct.unpack('>HH', rx_pdu[1:5])
# check server reply
if (resp_reg_addr != reg_addr) or (resp_reg_value != reg_value):
raise ModbusClient._NetworkError(MB_RECV_ERR, 'server reply does not match the request')
return True
# handle error during request
except ModbusClient._InternalError as e:
self._req_except_handler(e)
return False
def write_multiple_coils(self, bits_addr, bits_value):
"""Modbus function WRITE_MULTIPLE_COILS (0x0F).
:param bits_addr: bits address (0 to 65535)
:type bits_addr: int
:param bits_value: bits values to write
:type bits_value: list
:returns: True if write ok
:rtype: bool
"""
# check params
if not 0 <= int(bits_addr) <= 0xffff:
raise ValueError('bit_addr out of range (valid from 0 to 65535)')
if not 1 <= len(bits_value) <= 1968:
raise ValueError('number of coils out of range (valid from 1 to 1968)')
if int(bits_addr) + len(bits_value) > 0x10000:
raise ValueError('write after end of modbus address space')
# make request
try:
# build PDU coils part
# allocate a list of bytes
byte_l = [0] * byte_length(len(bits_value))
# populate byte list with coils values
for i, item in enumerate(bits_value):
if item:
byte_l[i // 8] = set_bit(byte_l[i // 8], i % 8)
# format PDU coils part with byte list
pdu_coils_part = struct.pack('%dB' % len(byte_l), *byte_l)
# concatenate PDU parts
tx_pdu = struct.pack('>BHHB', WRITE_MULTIPLE_COILS, bits_addr, len(bits_value), len(pdu_coils_part))
tx_pdu += pdu_coils_part
# make a request
rx_pdu = self._req_pdu(tx_pdu=tx_pdu, rx_min_len=5)
# response decode
resp_write_addr, resp_write_count = struct.unpack('>HH', rx_pdu[1:5])
# check response fields
write_ok = resp_write_addr == bits_addr and resp_write_count == len(bits_value)
return write_ok
# handle error during request
except ModbusClient._InternalError as e:
self._req_except_handler(e)
return False
def write_multiple_registers(self, regs_addr, regs_value):
"""Modbus function WRITE_MULTIPLE_REGISTERS (0x10).
:param regs_addr: registers address (0 to 65535)
:type regs_addr: int
:param regs_value: registers values to write
:type regs_value: list
:returns: True if write ok
:rtype: bool
"""
# check params
if not 0 <= int(regs_addr) <= 0xffff:
raise ValueError('regs_addr out of range (valid from 0 to 65535)')
if not 1 <= len(regs_value) <= 123:
raise ValueError('number of registers out of range (valid from 1 to 123)')
if int(regs_addr) + len(regs_value) > 0x10000:
raise ValueError('write after end of modbus address space')
# make request
try:
# init PDU registers part
pdu_regs_part = b''
# populate it with register values
for reg in regs_value:
# check current register value
if not 0 <= int(reg) <= 0xffff:
raise ValueError('regs_value list contains out of range values')
# pack register for build frame
pdu_regs_part += struct.pack('>H', reg)
bytes_nb = len(pdu_regs_part)
# concatenate PDU parts
tx_pdu = struct.pack('>BHHB', WRITE_MULTIPLE_REGISTERS, regs_addr, len(regs_value), bytes_nb)
tx_pdu += pdu_regs_part
# make a request
rx_pdu = self._req_pdu(tx_pdu=tx_pdu, rx_min_len=5)
# response decode
resp_write_addr, resp_write_count = struct.unpack('>HH', rx_pdu[1:5])
# check response fields
write_ok = resp_write_addr == regs_addr and resp_write_count == len(regs_value)
return write_ok
# handle error during request
except ModbusClient._InternalError as e:
self._req_except_handler(e)
return False
def write_read_multiple_registers(self, write_addr, write_values, read_addr, read_nb=1):
"""Modbus function WRITE_READ_MULTIPLE_REGISTERS (0x17).
:param write_addr: write registers address (0 to 65535)
:type write_addr: int
:param write_values: registers values to write
:type write_values: list
:param read_addr: read register address (0 to 65535)
:type read_addr: int
:param read_nb: number of registers to read (1 to 125)
:type read_nb: int
:returns: registers list or None if fail
:rtype: list of int or None
"""
# check params
check_l = [(not 0 <= int(write_addr) <= 0xffff, 'write_addr out of range (valid from 0 to 65535)'),
(not 1 <= len(write_values) <= 121, 'number of registers out of range (valid from 1 to 121)'),
(int(write_addr) + len(write_values) > 0x10000, 'write after end of modbus address space'),
(not 0 <= int(read_addr) <= 0xffff, 'read_addr out of range (valid from 0 to 65535)'),
(not 1 <= int(read_nb) <= 125, 'read_nb out of range (valid from 1 to 125)'),
(int(read_addr) + int(read_nb) > 0x10000, 'read after end of modbus address space'), ]
for err, msg in check_l:
if err:
raise ValueError(msg)
# make request
try:
# init PDU registers part
pdu_regs_part = b''
# populate it with register values
for reg in write_values:
# check current register value
if not 0 <= int(reg) <= 0xffff:
raise ValueError('write_values list contains out of range values')
# pack register for build frame
pdu_regs_part += struct.pack('>H', reg)
bytes_nb = len(pdu_regs_part)
# concatenate PDU parts
tx_pdu = struct.pack('>BHHHHB', WRITE_READ_MULTIPLE_REGISTERS, read_addr, read_nb,
write_addr, len(write_values), bytes_nb)
tx_pdu += pdu_regs_part
# make a request
rx_pdu = self._req_pdu(tx_pdu=tx_pdu, rx_min_len=4)
# response decode
# extract field "byte count"
byte_count = rx_pdu[1]
# frame with regs value
f_regs = rx_pdu[2:]
# check rx_byte_count: buffer size must be consistent and have at least the requested number of registers
if byte_count < 2 * read_nb or byte_count != len(f_regs):
raise ModbusClient._NetworkError(MB_RECV_ERR, 'rx byte count mismatch')
# allocate a reg_nb size list
registers = [0] * read_nb
# fill registers list with register items
for i in range(read_nb):
registers[i] = struct.unpack('>H', f_regs[i * 2:i * 2 + 2])[0]
# return registers list
return registers
# handle error during request
except ModbusClient._InternalError as e:
self._req_except_handler(e)
return
def _send(self, frame):
"""Send frame over current socket.
:param frame: modbus frame to send (MBAP + PDU)
:type frame: bytes
"""
# check socket
if not self.is_open:
raise ModbusClient._NetworkError(MB_SOCK_CLOSE_ERR, 'try to send on a close socket')
# send
try:
self._sock.send(frame)
except socket.timeout:
self._sock.close()
raise ModbusClient._NetworkError(MB_TIMEOUT_ERR, 'timeout error')
except socket.error:
self._sock.close()
raise ModbusClient._NetworkError(MB_SEND_ERR, 'send error')
def _send_pdu(self, pdu):
"""Convert modbus PDU to frame and send it.
:param pdu: modbus frame PDU
:type pdu: bytes
"""
# for auto_open mode, check TCP and open on need
if self.auto_open and not self.is_open:
self._open()
# add MBAP header to PDU
tx_frame = self._add_mbap(pdu)
# send frame with error check
self._send(tx_frame)
# debug
self._on_tx_rx(frame=tx_frame, is_tx=True)
def _recv(self, size):
"""Receive data over current socket.
:param size: number of bytes to receive
:type size: int
:returns: receive data or None if error
:rtype: bytes
"""
try:
r_buffer = self._sock.recv(size)
except socket.timeout:
self._sock.close()
raise ModbusClient._NetworkError(MB_TIMEOUT_ERR, 'timeout error')
except socket.error:
r_buffer = b''
# handle recv error
if not r_buffer:
self._sock.close()
raise ModbusClient._NetworkError(MB_RECV_ERR, 'recv error')
return r_buffer
def _recv_all(self, size):
"""Receive data over current socket, loop until all bytes is received (avoid TCP frag).
:param size: number of bytes to receive
:type size: int
:returns: receive data or None if error
:rtype: bytes
"""
r_buffer = b''
while len(r_buffer) < size:
r_buffer += self._recv(size - len(r_buffer))
return r_buffer
def _recv_pdu(self, min_len=2):
"""Receive the modbus PDU (Protocol Data Unit).
:param min_len: minimal length of the PDU
:type min_len: int
:returns: modbus frame PDU or None if error
:rtype: bytes or None
"""
# receive 7 bytes header (MBAP)
rx_mbap = self._recv_all(7)
# decode MBAP
(f_transaction_id, f_protocol_id, f_length, f_unit_id) = struct.unpack('>HHHB', rx_mbap)
# check MBAP fields
f_transaction_err = f_transaction_id != self._transaction_id
f_protocol_err = f_protocol_id != 0
f_length_err = f_length >= 256
f_unit_id_err = f_unit_id != self.unit_id
# checking error status of fields
if f_transaction_err or f_protocol_err or f_length_err or f_unit_id_err:
self.close()
self._on_tx_rx(frame=rx_mbap, is_tx=False)
raise ModbusClient._NetworkError(MB_RECV_ERR, 'MBAP checking error')
# recv PDU
rx_pdu = self._recv_all(f_length - 1)
# for auto_close mode, close socket after each request
if self.auto_close:
self.close()
# dump frame
self._on_tx_rx(frame=rx_mbap + rx_pdu, is_tx=False)
# body decode
# check PDU length for global minimal frame (an except frame: func code + exp code)
if len(rx_pdu) < 2:
raise ModbusClient._NetworkError(MB_RECV_ERR, 'PDU length is too short')
# extract function code
rx_fc = rx_pdu[0]
# check except status
if rx_fc >= 0x80:
exp_code = rx_pdu[1]
raise ModbusClient._ModbusExcept(exp_code)
# check PDU length for specific request set in min_len (keep this after except checking)
if len(rx_pdu) < min_len:
raise ModbusClient._NetworkError(MB_RECV_ERR, 'PDU length is too short for current request')
# if no error, return PDU
return rx_pdu
def _add_mbap(self, pdu):
"""Return full modbus frame with MBAP (modbus application protocol header) append to PDU.
:param pdu: modbus PDU (protocol data unit)
:type pdu: bytes
:returns: full modbus frame
:rtype: bytes
"""
# build MBAP
self._transaction_id = random.randint(0, 65535)
protocol_id = 0
length = len(pdu) + 1
mbap = struct.pack('>HHHB', self._transaction_id, protocol_id, length, self.unit_id)
# full modbus/TCP frame = [MBAP]PDU
return mbap + pdu
def _req_pdu(self, tx_pdu, rx_min_len=2):
"""Request processing (send and recv PDU).
:param tx_pdu: modbus PDU (protocol data unit) to send
:type tx_pdu: bytes
:param rx_min_len: min length of receive PDU
:type rx_min_len: int
:returns: the receive PDU or None if error
:rtype: bytes
"""
# init request engine
self._req_init()
# send PDU
self._send_pdu(tx_pdu)
# return receive PDU
return self._recv_pdu(min_len=rx_min_len)
def _req_init(self):
"""Reset request status flags."""
self._last_error = MB_NO_ERR
self._last_except = EXP_NONE
def _req_except_handler(self, _except):
"""Global handler for internal exceptions."""
# on request network error
if isinstance(_except, ModbusClient._NetworkError):
self._last_error = _except.code
self._debug_msg(_except.message)
# on request modbus except
if isinstance(_except, ModbusClient._ModbusExcept):
self._last_error = MB_EXCEPT_ERR
self._last_except = _except.code
self._debug_msg(f'modbus exception (code {self.last_except} "{self.last_error_as_txt}")')
def _debug_msg(self, msg: str):
logger.debug(f'({self.host}:{self.port}:{self.unit_id}) {msg}')
def _on_tx_rx(self, frame: bytes, is_tx: bool):
# format a log message
if logger.isEnabledFor(logging.DEBUG):
type_s = 'Tx' if is_tx else 'Rx'
mbap_s = hexlify(frame[0:7], sep=' ').upper().decode()
pdu_s = hexlify(frame[7:], sep=' ').upper().decode()
self._debug_msg(f'{type_s} [{mbap_s}] {pdu_s}')
# notify user
self.on_tx_rx(frame=frame, is_tx=is_tx)
def on_tx_rx(self, frame: bytes, is_tx: bool):
"""Call for each Tx/Rx (for user purposes)."""
pass
================================================
FILE: pyModbusTCP/constants.py
================================================
""" pyModbusTCP package constants definition """
# Package version
VERSION = '0.3.1.dev0'
# Modbus/TCP
MODBUS_PORT = 502
# Modbus function code
READ_COILS = 0x01
READ_DISCRETE_INPUTS = 0x02
READ_HOLDING_REGISTERS = 0x03
READ_INPUT_REGISTERS = 0x04
WRITE_SINGLE_COIL = 0x05
WRITE_SINGLE_REGISTER = 0x06
WRITE_MULTIPLE_COILS = 0x0F
WRITE_MULTIPLE_REGISTERS = 0x10
WRITE_READ_MULTIPLE_REGISTERS = 0x17
ENCAPSULATED_INTERFACE_TRANSPORT = 0x2B
SUPPORTED_FUNCTION_CODES = (READ_COILS, READ_DISCRETE_INPUTS, READ_HOLDING_REGISTERS, READ_INPUT_REGISTERS,
WRITE_SINGLE_COIL, WRITE_SINGLE_REGISTER, WRITE_MULTIPLE_COILS, WRITE_MULTIPLE_REGISTERS,
WRITE_READ_MULTIPLE_REGISTERS, ENCAPSULATED_INTERFACE_TRANSPORT)
# MEI type
MEI_TYPE_READ_DEVICE_ID = 0x0E
# Modbus except code
EXP_NONE = 0x00
EXP_ILLEGAL_FUNCTION = 0x01
EXP_DATA_ADDRESS = 0x02
EXP_DATA_VALUE = 0x03
EXP_SLAVE_DEVICE_FAILURE = 0x04
EXP_ACKNOWLEDGE = 0x05
EXP_SLAVE_DEVICE_BUSY = 0x06
EXP_NEGATIVE_ACKNOWLEDGE = 0x07
EXP_MEMORY_PARITY_ERROR = 0x08
EXP_GATEWAY_PATH_UNAVAILABLE = 0x0A
EXP_GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND = 0x0B
# Exception as short human-readable
EXP_TXT = {
EXP_NONE: 'no exception',
EXP_ILLEGAL_FUNCTION: 'illegal function',
EXP_DATA_ADDRESS: 'illegal data address',
EXP_DATA_VALUE: 'illegal data value',
EXP_SLAVE_DEVICE_FAILURE: 'slave device failure',
EXP_ACKNOWLEDGE: 'acknowledge',
EXP_SLAVE_DEVICE_BUSY: 'slave device busy',
EXP_NEGATIVE_ACKNOWLEDGE: 'negative acknowledge',
EXP_MEMORY_PARITY_ERROR: 'memory parity error',
EXP_GATEWAY_PATH_UNAVAILABLE: 'gateway path unavailable',
EXP_GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND: 'gateway target device failed to respond'
}
# Exception as details human-readable
EXP_DETAILS = {
EXP_NONE: 'The last request produced no exceptions.',
EXP_ILLEGAL_FUNCTION: 'Function code received in the query is not recognized or allowed by slave.',
EXP_DATA_ADDRESS: 'Data address of some or all the required entities are not allowed or do not exist in slave.',
EXP_DATA_VALUE: 'Value is not accepted by slave.',
EXP_SLAVE_DEVICE_FAILURE: 'Unrecoverable error occurred while slave was attempting to perform requested action.',
EXP_ACKNOWLEDGE: 'Slave has accepted request and is processing it, but a long duration of time is required. '
'This response is returned to prevent a timeout error from occurring in the master. '
'Master can next issue a Poll Program Complete message to determine whether processing '
'is completed.',
EXP_SLAVE_DEVICE_BUSY: 'Slave is engaged in processing a long-duration command. Master should retry later.',
EXP_NEGATIVE_ACKNOWLEDGE: 'Slave cannot perform the programming functions. '
'Master should request diagnostic or error information from slave.',
EXP_MEMORY_PARITY_ERROR: 'Slave detected a parity error in memory. '
'Master can retry the request, but service may be required on the slave device.',
EXP_GATEWAY_PATH_UNAVAILABLE: 'Specialized for Modbus gateways, this indicates a misconfiguration on gateway.',
EXP_GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND: 'Specialized for Modbus gateways, sent when slave fails to respond.'
}
# Module error codes
MB_NO_ERR = 0
MB_RESOLVE_ERR = 1
MB_CONNECT_ERR = 2
MB_SEND_ERR = 3
MB_RECV_ERR = 4
MB_TIMEOUT_ERR = 5
MB_FRAME_ERR = 6
MB_EXCEPT_ERR = 7
MB_CRC_ERR = 8
MB_SOCK_CLOSE_ERR = 9
# Module error as short human-readable
MB_ERR_TXT = {
MB_NO_ERR: 'no error',
MB_RESOLVE_ERR: 'name resolve error',
MB_CONNECT_ERR: 'connect error',
MB_SEND_ERR: 'socket send error',
MB_RECV_ERR: 'socket recv error',
MB_TIMEOUT_ERR: 'recv timeout occur',
MB_FRAME_ERR: 'frame format error',
MB_EXCEPT_ERR: 'modbus exception',
MB_CRC_ERR: 'bad CRC on receive frame',
MB_SOCK_CLOSE_ERR: 'socket is closed'
}
# Misc
MAX_PDU_SIZE = 253
================================================
FILE: pyModbusTCP/server.py
================================================
""" pyModbusTCP Server """
import logging
import socket
import struct
from socketserver import BaseRequestHandler, ThreadingTCPServer
from threading import Event, Lock, Thread
from warnings import warn
from .constants import (ENCAPSULATED_INTERFACE_TRANSPORT, EXP_DATA_ADDRESS,
EXP_DATA_VALUE, EXP_ILLEGAL_FUNCTION, EXP_NONE,
MAX_PDU_SIZE, MEI_TYPE_READ_DEVICE_ID, READ_COILS,
READ_DISCRETE_INPUTS, READ_HOLDING_REGISTERS,
READ_INPUT_REGISTERS, WRITE_MULTIPLE_COILS,
WRITE_MULTIPLE_REGISTERS,
WRITE_READ_MULTIPLE_REGISTERS, WRITE_SINGLE_COIL,
WRITE_SINGLE_REGISTER)
from .utils import set_bit, test_bit
# add a logger for pyModbusTCP.server
logger = logging.getLogger(__name__)
class DataBank:
""" Data space class with thread safe access functions """
_DEPR_MSG = 'This class method is deprecated. Use DataBank instance method instead: '
@classmethod
def get_bits(cls, *_args, **_kwargs):
msg = DataBank._DEPR_MSG + 'server.data_bank.get_coils() or get_discrete_inputs()'
warn(msg, DeprecationWarning, stacklevel=2)
@classmethod
def set_bits(cls, *_args, **_kwargs):
msg = DataBank._DEPR_MSG + 'server.data_bank.set_coils() or set_discrete_inputs()'
warn(msg, DeprecationWarning, stacklevel=2)
@classmethod
def get_words(cls, *_args, **_kwargs):
msg = DataBank._DEPR_MSG + 'server.data_bank.get_holding_registers() or get_input_registers()'
warn(msg, DeprecationWarning, stacklevel=2)
@classmethod
def set_words(cls, *_args, **_kwargs):
msg = DataBank._DEPR_MSG + 'server.data_bank.set_holding_registers() or set_input_registers()'
warn(msg, DeprecationWarning, stacklevel=2)
def __init__(self, coils_size=0x10000, coils_default_value=False,
d_inputs_size=0x10000, d_inputs_default_value=False,
h_regs_size=0x10000, h_regs_default_value=0,
i_regs_size=0x10000, i_regs_default_value=0,
virtual_mode=False):
"""Constructor
Modbus server data bank constructor.
:param coils_size: Number of coils to allocate (default is 65536)
:type coils_size: int
:param coils_default_value: Coils default value at startup (default is False)
:type coils_default_value: bool
:param d_inputs_size: Number of discrete inputs to allocate (default is 65536)
:type d_inputs_size: int
:param d_inputs_default_value: Discrete inputs default value at startup (default is False)
:type d_inputs_default_value: bool
:param h_regs_size: Number of holding registers to allocate (default is 65536)
:type h_regs_size: int
:param h_regs_default_value: Holding registers default value at startup (default is 0)
:type h_regs_default_value: int
:param i_regs_size: Number of input registers to allocate (default is 65536)
:type i_regs_size: int
:param i_regs_default_value: Input registers default value at startup (default is 0)
:type i_regs_default_value: int
:param virtual_mode: Disallow all modbus data space to work with virtual values (default is False)
:type virtual_mode: bool
"""
# public
self.coils_size = int(coils_size)
self.coils_default_value = bool(coils_default_value)
self.d_inputs_size = int(d_inputs_size)
self.d_inputs_default_value = bool(d_inputs_default_value)
self.h_regs_size = int(h_regs_size)
self.h_regs_default_value = int(h_regs_default_value)
self.i_regs_size = int(i_regs_size)
self.i_regs_default_value = int(i_regs_default_value)
self.virtual_mode = virtual_mode
# specific modes (override some values)
if self.virtual_mode:
self.coils_size = 0
self.d_inputs_size = 0
self.h_regs_size = 0
self.i_regs_size = 0
# private
self._coils_lock = Lock()
self._coils = [self.coils_default_value] * self.coils_size
self._d_inputs_lock = Lock()
self._d_inputs = [self.d_inputs_default_value] * self.d_inputs_size
self._h_regs_lock = Lock()
self._h_regs = [self.h_regs_default_value] * self.h_regs_size
self._i_regs_lock = Lock()
self._i_regs = [self.i_regs_default_value] * self.i_regs_size
def __repr__(self):
attrs_str = ''
for attr_name in self.__dict__:
if isinstance(attr_name, str) and not attr_name.startswith('_'):
if attrs_str:
attrs_str += ', '
attrs_str += '%s=%r' % (attr_name, self.__dict__[attr_name])
return 'DataBank(%s)' % attrs_str
def get_coils(self, address, number=1, srv_info=None):
"""Read data on server coils space
:param address: start address
:type address: int
:param number: number of bits (optional)
:type number: int
:param srv_info: some server info (must be set by server only)
:type srv_info: ModbusServer.ServerInfo
:returns: list of bool or None if error
:rtype: list or None
"""
# secure extract of data from list used by server thread
with self._coils_lock:
if (address >= 0) and (address + number <= len(self._coils)):
return self._coils[address: number + address]
else:
return None
def set_coils(self, address, bit_list, srv_info=None):
"""Write data to server coils space
:param address: start address
:type address: int
:param bit_list: a list of bool to write
:type bit_list: list
:param srv_info: some server info (must be set by server only)
:type srv_info: ModbusServerInfo
:returns: True if success or None if error
:rtype: bool or None
:raises ValueError: if bit_list members cannot be converted to bool
"""
# ensure bit_list values are bool
bit_list = [bool(b) for b in bit_list]
# keep trace of any changes
changes_list = []
# ensure atomic update of internal data
with self._coils_lock:
if (address >= 0) and (address + len(bit_list) <= len(self._coils)):
for offset, c_value in enumerate(bit_list):
c_address = address + offset
if self._coils[c_address] != c_value:
changes_list.append((c_address, self._coils[c_address], c_value))
self._coils[c_address] = c_value
else:
return None
# on server update
if srv_info:
# notify changes with on change method (after atomic update)
for address, from_value, to_value in changes_list:
self.on_coils_change(address, from_value, to_value, srv_info)
return True
def get_discrete_inputs(self, address, number=1, srv_info=None):
"""Read data on server discrete inputs space
:param address: start address
:type address: int
:param number: number of bits (optional)
:type number: int
:param srv_info: some server info (must be set by server only)
:type srv_info: ModbusServerInfo
:returns: list of bool or None if error
:rtype: list or None
"""
# secure extract of data from list used by server thread
with self._d_inputs_lock:
if (address >= 0) and (address + number <= len(self._d_inputs)):
return self._d_inputs[address: number + address]
else:
return None
def set_discrete_inputs(self, address, bit_list):
"""Write data to server discrete inputs space
:param address: start address
:type address: int
:param bit_list: a list of bool to write
:type bit_list: list
:returns: True if success or None if error
:rtype: bool or None
:raises ValueError: if bit_list members cannot be converted to bool
"""
# ensure bit_list values are bool
bit_list = [bool(b) for b in bit_list]
# ensure atomic update of internal data
with self._d_inputs_lock:
if (address >= 0) and (address + len(bit_list) <= len(self._d_inputs)):
for offset, b_value in enumerate(bit_list):
self._d_inputs[address + offset] = b_value
else:
return None
return True
def get_holding_registers(self, address, number=1, srv_info=None):
"""Read data on server holding registers space
:param address: start address
:type address: int
:param number: number of words (optional)
:type number: int
:param srv_info: some server info (must be set by server only)
:type srv_info: ModbusServerInfo
:returns: list of int or None if error
:rtype: list or None
"""
# secure extract of data from list used by server thread
with self._h_regs_lock:
if (address >= 0) and (address + number <= len(self._h_regs)):
return self._h_regs[address: number + address]
else:
return None
def set_holding_registers(self, address, word_list, srv_info=None):
"""Write data to server holding registers space
:param address: start address
:type address: int
:param word_list: a list of word to write
:type word_list: list
:param srv_info: some server info (must be set by server only)
:type srv_info: ModbusServerInfo
:returns: True if success or None if error
:rtype: bool or None
:raises ValueError: if word_list members cannot be converted to int
"""
# ensure word_list values are int with a max bit length of 16
word_list = [int(w) & 0xffff for w in word_list]
# keep trace of any changes
changes_list = []
# ensure atomic update of internal data
with self._h_regs_lock:
if (address >= 0) and (address + len(word_list) <= len(self._h_regs)):
for offset, c_value in enumerate(word_list):
c_address = address + offset
if self._h_regs[c_address] != c_value:
changes_list.append((c_address, self._h_regs[c_address], c_value))
self._h_regs[c_address] = c_value
else:
return None
# on server update
if srv_info:
# notify changes with on change method (after atomic update)
for address, from_value, to_value in changes_list:
self.on_holding_registers_change(address, from_value, to_value, srv_info=srv_info)
return True
def get_input_registers(self, address, number=1, srv_info=None):
"""Read data on server input registers space
:param address: start address
:type address: int
:param number: number of words (optional)
:type number: int
:param srv_info: some server info (must be set by server only)
:type srv_info: ModbusServerInfo
:returns: list of int or None if error
:rtype: list or None
"""
# secure extract of data from list used by server thread
with self._i_regs_lock:
if (address >= 0) and (address + number <= len(self._i_regs)):
return self._i_regs[address: number + address]
else:
return None
def set_input_registers(self, address, word_list):
"""Write data to server input registers space
:param address: start address
:type address: int
:param word_list: a list of word to write
:type word_list: list
:returns: True if success or None if error
:rtype: bool or None
:raises ValueError: if word_list members cannot be converted to int
"""
# ensure word_list values are int with a max bit length of 16
word_list = [int(w) & 0xffff for w in word_list]
# ensure atomic update of internal data
with self._i_regs_lock:
if (address >= 0) and (address + len(word_list) <= len(self._i_regs)):
for offset, c_value in enumerate(word_list):
c_address = address + offset
if self._i_regs[c_address] != c_value:
self._i_regs[c_address] = c_value
else:
return None
return True
def on_coils_change(self, address, from_value, to_value, srv_info):
"""Call by server when a value change occur in coils space
This method is provided to be overridden with user code to catch changes
:param address: address of coil
:type address: int
:param from_value: coil original value
:type from_value: bool
:param to_value: coil next value
:type to_value: bool
:param srv_info: some server info
:type srv_info: ModbusServerInfo
"""
pass
def on_holding_registers_change(self, address, from_value, to_value, srv_info):
"""Call by server when a value change occur in holding registers space
This method is provided to be overridden with user code to catch changes
:param address: address of register
:type address: int
:param from_value: register original value
:type from_value: int
:param to_value: register next value
:type to_value: int
:param srv_info: some server info
:type srv_info: ModbusServerInfo
"""
pass
class DataHandler:
"""Default data handler for ModbusServer, map server threads calls to DataBank.
Custom handler must derive from this class.
"""
class Return:
def __init__(self, exp_code, data=None):
self.exp_code = exp_code
self.data = data
@property
def ok(self):
return self.exp_code == EXP_NONE
def __init__(self, data_bank=None):
"""Constructor
Modbus server data handler constructor.
:param data_bank: a reference to custom DefaultDataBank
:type data_bank: DataBank
"""
# check data_bank type
if data_bank and not isinstance(data_bank, DataBank):
raise TypeError('data_bank arg is invalid')
# public
self.data_bank = data_bank or DataBank()
def __repr__(self):
return 'ModbusServerDataHandler(data_bank=%s)' % self.data_bank
def read_coils(self, address, count, srv_info):
"""Call by server for reading in coils space
:param address: start address
:type address: int
:param count: number of coils
:type count: int
:param srv_info: some server info
:type srv_info: ModbusServer.ServerInfo
:rtype: Return
"""
# read bits from DataBank
bits_l = self.data_bank.get_coils(address, count, srv_info)
# return DataStatus to server
if bits_l is not None:
return DataHandler.Return(exp_code=EXP_NONE, data=bits_l)
else:
return DataHandler.Return(exp_code=EXP_DATA_ADDRESS)
def write_coils(self, address, bits_l, srv_info):
"""Call by server for writing in the coils space
:param address: start address
:type address: int
:param bits_l: list of boolean to write
:type bits_l: list
:param srv_info: some server info
:type srv_info: ModbusServer.ServerInfo
:rtype: Return
"""
# write bits to DataBank
update_ok = self.data_bank.set_coils(address, bits_l, srv_info)
# return DataStatus to server
if update_ok:
return DataHandler.Return(exp_code=EXP_NONE)
else:
return DataHandler.Return(exp_code=EXP_DATA_ADDRESS)
def read_d_inputs(self, address, count, srv_info):
"""Call by server for reading in the discrete inputs space
:param address: start address
:type address: int
:param count: number of discrete inputs
:type count: int
:param srv_info: some server info
:type srv_info: ModbusServer.ServerInfo
:rtype: Return
"""
# read bits from DataBank
bits_l = self.data_bank.get_discrete_inputs(address, count, srv_info)
# return DataStatus to server
if bits_l is not None:
return DataHandler.Return(exp_code=EXP_NONE, data=bits_l)
else:
return DataHandler.Return(exp_code=EXP_DATA_ADDRESS)
def read_h_regs(self, address, count, srv_info):
"""Call by server for reading in the holding registers space
:param address: start address
:type address: int
:param count: number of holding registers
:type count: int
:param srv_info: some server info
:type srv_info: ModbusServer.ServerInfo
:rtype: Return
"""
# read words from DataBank
words_l = self.data_bank.get_holding_registers(address, count, srv_info)
# return DataStatus to server
if words_l is not None:
return DataHandler.Return(exp_code=EXP_NONE, data=words_l)
else:
return DataHandler.Return(exp_code=EXP_DATA_ADDRESS)
def write_h_regs(self, address, words_l, srv_info):
"""Call by server for writing in the holding registers space
:param address: start address
:type address: int
:param words_l: list of word value to write
:type words_l: list
:param srv_info: some server info
:type srv_info: ModbusServer.ServerInfo
:rtype: Return
"""
# write words to DataBank
update_ok = self.data_bank.set_holding_registers(address, words_l, srv_info)
# return DataStatus to server
if update_ok:
return DataHandler.Return(exp_code=EXP_NONE)
else:
return DataHandler.Return(exp_code=EXP_DATA_ADDRESS)
def read_i_regs(self, address, count, srv_info):
"""Call by server for reading in the input registers space
:param address: start address
:type address: int
:param count: number of input registers
:type count: int
:param srv_info: some server info
:type srv_info: ModbusServer.ServerInfo
:rtype: Return
"""
# read words from DataBank
words_l = self.data_bank.get_input_registers(address, count, srv_info)
# return DataStatus to server
if words_l is not None:
return DataHandler.Return(exp_code=EXP_NONE, data=words_l)
else:
return DataHandler.Return(exp_code=EXP_DATA_ADDRESS)
class DeviceIdentification:
""" Container class for device identification objects (MEI type 0x0E) return by function 0x2B. """
def __init__(self, vendor_name=b'', product_code=b'', major_minor_revision=b'', vendor_url=b'',
product_name=b'', model_name=b'', user_application_name=b'', objects_id=None):
"""
Constructor
:param vendor_name: VendorName mandatory object
:type vendor_name: bytes
:param product_code: ProductCode mandatory object
:type product_code: bytes
:param major_minor_revision: MajorMinorRevision mandatory object
:type major_minor_revision: bytes
:param vendor_url: VendorUrl regular object
:type vendor_url: bytes
:param product_name: ProductName regular object
:type product_name: bytes
:param model_name: ModelName regular object
:type model_name: bytes
:param user_application_name: UserApplicationName regular object
:type user_application_name: bytes
:param objects_id: Objects values by id as dict example: {42:b'value'} (optional)
:type objects_id: dict
"""
# private
self._objs_d = {}
self._objs_lock = Lock()
# default values
self.vendor_name = vendor_name
self.product_code = product_code
self.major_minor_revision = major_minor_revision
self.vendor_url = vendor_url
self.product_name = product_name
self.model_name = model_name
self.user_application_name = user_application_name
# process objects_id dict (populate no name objects)
if isinstance(objects_id, dict):
for key, value in objects_id.items():
self[key] = value
def __getitem__(self, key):
if not isinstance(key, int):
raise TypeError('key must be an int')
with self._objs_lock:
return self._objs_d[key]
def __setitem__(self, key, value):
if not isinstance(key, int):
raise TypeError('key must be an int')
if 0xff >= key >= 0x00:
if not isinstance(value, bytes):
raise TypeError('this object is of type bytes only')
with self._objs_lock:
self._objs_d[key] = value
else:
raise ValueError('key not in valid range (0 to 255)')
def __repr__(self):
named_params = ''
# add named parameters
for prop_name in ('vendor_name', 'product_code', 'major_minor_revision', 'vendor_url',
'product_name', 'model_name', 'user_application_name'):
prop_value = getattr(self, prop_name)
if prop_value:
if named_params:
named_params += ', '
named_params += '%s=%r' % (prop_name, getattr(self, prop_name))
# add parameters without shortcut name
objs_id_d_str = ''
for _id in range(0x07, 0x100):
try:
obj_id_item = '%r: %r' % (_id, self[_id])
if objs_id_d_str:
objs_id_d_str += ', '
objs_id_d_str += obj_id_item
except KeyError:
pass
# format str: classname(params_name=value, ..., objects_id={42: 'value'})
class_args = named_params
if objs_id_d_str:
if class_args:
class_args += ', '
class_args += 'objects_id={%s}' % objs_id_d_str
return '%s(%s)' % (self.__class__.__name__, class_args)
@property
def vendor_name(self):
return self[0]
@vendor_name.setter
def vendor_name(self, value):
self[0] = value
@property
def product_code(self):
return self[1]
@product_code.setter
def product_code(self, value):
self[1] = value
@property
def major_minor_revision(self):
return self[2]
@major_minor_revision.setter
def major_minor_revision(self, value):
self[2] = value
@property
def vendor_url(self):
return self[3]
@vendor_url.setter
def vendor_url(self, value):
self[3] = value
@property
def product_name(self):
return self[4]
@product_name.setter
def product_name(self, value):
self[4] = value
@property
def model_name(self):
return self[5]
@model_name.setter
def model_name(self, value):
self[5] = value
@property
def user_application_name(self):
return self[6]
@user_application_name.setter
def user_application_name(self, value):
self[6] = value
def items(self, start=0x00, end=0xff):
items_l = []
for obj_id in range(start, end + 1):
try:
items_l.append((obj_id, self[obj_id]))
except KeyError:
pass
return items_l
class ModbusServer:
""" Modbus TCP server """
class Error(Exception):
""" Base exception for ModbusServer related errors. """
pass
class NetworkError(Error):
""" Exception raise by ModbusServer on I/O errors. """
pass
class DataFormatError(Error):
""" Exception raise by ModbusServer for data format errors. """
pass
class ClientInfo:
""" Container class for client information """
def __init__(self, address='', port=0):
self.address = address
self.port = port
def __repr__(self):
return 'ClientInfo(address=%r, port=%r)' % (self.address, self.port)
class ServerInfo:
""" Container class for server information """
def __init__(self):
self.client = ModbusServer.ClientInfo()
self.recv_frame = ModbusServer.Frame()
class SessionData:
""" Container class for server session data. """
def __init__(self):
self.client = ModbusServer.ClientInfo()
self.request = ModbusServer.Frame()
self.response = ModbusServer.Frame()
@property
def srv_info(self):
info = ModbusServer.ServerInfo()
info.client = self.client
info.recv_frame = self.request
return info
def new_request(self):
self.request = ModbusServer.Frame()
self.response = ModbusServer.Frame()
def set_response_mbap(self):
self.response.mbap.transaction_id = self.request.mbap.transaction_id
self.response.mbap.protocol_id = self.request.mbap.protocol_id
self.response.mbap.unit_id = self.request.mbap.unit_id
class Frame:
def __init__(self):
""" Modbus Frame container. """
self.mbap = ModbusServer.MBAP()
self.pdu = ModbusServer.PDU()
@property
def raw(self):
self.mbap.length = len(self.pdu) + 1
return self.mbap.raw + self.pdu.raw
class MBAP:
""" MBAP (Modbus Application Protocol) container class. """
def __init__(self, transaction_id=0, protocol_id=0, length=0, unit_id=0):
# public
self.transaction_id = transaction_id
self.protocol_id = protocol_id
self.length = length
self.unit_id = unit_id
@property
def raw(self):
try:
return struct.pack('>HHHB', self.transaction_id,
self.protocol_id, self.length,
self.unit_id)
except struct.error as e:
raise ModbusServer.DataFormatError('MBAP raw encode pack error: %s' % e)
@raw.setter
def raw(self, value):
# close connection if no standard 7 bytes mbap header
if not (value and len(value) == 7):
raise ModbusServer.DataFormatError('MBAP must have a length of 7 bytes')
# decode header
(self.transaction_id, self.protocol_id,
self.length, self.unit_id) = struct.unpack('>HHHB', value)
# check frame header content inconsistency
if self.protocol_id != 0:
raise ModbusServer.DataFormatError('MBAP protocol ID must be 0')
if not 2 < self.length < 256:
raise ModbusServer.DataFormatError('MBAP length must be between 2 and 256')
class PDU:
""" PDU (Protocol Data Unit) container class. """
def __init__(self, raw=b''):
"""
Constructor
:param raw: raw PDU
:type raw: bytes
"""
self.raw = raw
def __len__(self):
return len(self.raw)
@property
def func_code(self):
return self.raw[0]
@property
def except_code(self):
return self.raw[1]
@property
def is_except(self):
return self.func_code > 0x7F
@property
def is_valid(self):
# PDU min length is 2 bytes
return self.__len__() < 2
def clear(self):
self.raw = b''
def build_except(self, func_code, exp_status):
self.clear()
self.add_pack('BB', func_code + 0x80, exp_status)
return self
def add_pack(self, fmt, *args):
try:
self.raw += struct.pack(fmt, *args)
except struct.error:
err_msg = 'unable to format PDU message (fmt: %s, values: %s)' % (fmt, args)
raise ModbusServer.DataFormatError(err_msg)
def unpack(self, fmt, from_byte=None, to_byte=None):
raw_section = self.raw[from_byte:to_byte]
try:
return struct.unpack(fmt, raw_section)
except struct.error:
err_msg = 'unable to decode PDU message (fmt: %s, values: %s)' % (fmt, raw_section)
raise ModbusServer.DataFormatError(err_msg)
class ModbusService(BaseRequestHandler):
@property
def server_running(self):
return self.server.evt_running.is_set()
def _send_all(self, data):
try:
self.request.sendall(data)
return True
except socket.timeout:
return False
def _recv_all(self, size):
data = b''
while len(data) < size:
try:
# avoid keeping this TCP thread run after server.stop() on main server
if not self.server_running:
raise ModbusServer.NetworkError('main server is not running')
# recv all data or a chunk of it
data_chunk = self.request.recv(size - len(data))
# check data chunk
if data_chunk:
data += data_chunk
else:
raise ModbusServer.NetworkError('recv return null')
except socket.timeout:
# just redo main server run test and recv operations on timeout
pass
return data
def setup(self):
# set a socket timeout of 1s on blocking operations (like send/recv)
# this avoids hang thread deletion when main server exit (see _recv_all method)
self.request.settimeout(1.0)
def handle(self):
# try/except: end current thread on ModbusServer._InternalError, OSError or socket.error
# this also close the current TCP session associated with it
try:
# init and update server info structure
session_data = ModbusServer.SessionData()
(session_data.client.address, session_data.client.port) = self.request.getpeername()
# debug message
logger.debug('accept new connection from %r', session_data.client)
# main processing loop
while True:
# init session data for new request
session_data.new_request()
# receive mbap from client
session_data.request.mbap.raw = self._recv_all(7)
# receive pdu from client
session_data.request.pdu.raw = self._recv_all(session_data.request.mbap.length - 1)
# update response MBAP fields with request data
session_data.set_response_mbap()
# pass the current session data to request engine
self.server.engine(session_data)
# send the tx pdu with the last rx mbap (only length field change)
self._send_all(session_data.response.raw)
except (ModbusServer.Error, OSError, socket.error) as e:
# debug message
logger.debug('Exception during request handling: %r', e)
# on main loop except: exit from it and cleanly close the current socket
self.request.close()
def __init__(self, host='localhost', port=502, no_block=False, ipv6=False,
data_bank=None, data_hdl=None, ext_engine=None, device_id=None):
"""Constructor
Modbus server constructor.
:param host: hostname or IPv4/IPv6 address server address (default is 'localhost')
:type host: str
:param port: TCP port number (default is 502)
:type port: int
:param no_block: no block mode, i.e. start() will return (default is False)
:type no_block: bool
:param ipv6: use ipv6 stack (default is False)
:type ipv6: bool
:param data_bank: instance of custom data bank, if you don't want the default one (optional)
:type data_bank: DataBank
:param data_hdl: instance of custom data handler, if you don't want the default one (optional)
:type data_hdl: DataHandler
:param ext_engine: an external engine reference (ref to ext_engine(session_data)) (optional)
:type ext_engine: callable
:param device_id: instance of DeviceIdentification class for read device identification request (optional)
:type device_id: DeviceIdentification
"""
# check data_bank
if data_bank and not isinstance(data_bank, DataBank):
raise TypeError('data_bank is not a DataBank instance')
# check data_hdl
if data_hdl and not isinstance(data_hdl, DataHandler):
raise TypeError('data_hdl is not a DataHandler instance')
# data_hdl and data_bank can't be set at same time
if data_hdl and data_bank:
raise ValueError('when data_hdl is set, you must define data_bank in it')
# check ext_engine
if ext_engine and not callable(ext_engine):
raise TypeError('ext_engine must be callable')
# check device_id
if device_id and not isinstance(device_id, DeviceIdentification):
raise TypeError('device_id is not a DeviceIdentification instance')
# public
self.host = host
self.port = port
self.no_block = no_block
self.ipv6 = ipv6
self.ext_engine = ext_engine
# First, internal data_bank will be linked to an external data handler if defined.
# If not, an external or internal DataBank will be used instead.
# "virtual mode" will be set for save memory if an external engine is in use.
self.data_bank = data_hdl.data_bank if data_hdl else data_bank or DataBank(virtual_mode=bool(ext_engine))
self.data_hdl = data_hdl or DataHandler(data_bank=self.data_bank)
self.device_id = device_id
# private
self._evt_running = Event()
self._service = None
self._serve_th = None
# modbus default functions map
self._func_map = {READ_COILS: self._read_bits,
READ_DISCRETE_INPUTS: self._read_bits,
READ_HOLDING_REGISTERS: self._read_words,
READ_INPUT_REGISTERS: self._read_words,
WRITE_SINGLE_COIL: self._write_single_coil,
WRITE_SINGLE_REGISTER: self._write_single_register,
WRITE_MULTIPLE_COILS: self._write_multiple_coils,
WRITE_MULTIPLE_REGISTERS: self._write_multiple_registers,
WRITE_READ_MULTIPLE_REGISTERS: self._write_read_multiple_registers,
ENCAPSULATED_INTERFACE_TRANSPORT: self._encapsulated_interface_transport}
def __repr__(self):
r_str = 'ModbusServer(host=\'%s\', port=%d, no_block=%s, ipv6=%s, data_bank=%s, data_hdl=%s, ext_engine=%s)'
r_str %= (self.host, self.port, self.no_block, self.ipv6, self.data_bank, self.data_hdl, self.ext_engine)
return r_str
def _engine(self, session_data):
"""Main request processing engine.
:type session_data: ModbusServer.SessionData
"""
# call external engine or internal one (if ext_engine undefined)
if callable(self.ext_engine):
try:
self.ext_engine(session_data)
except Exception as e:
raise ModbusServer.Error('external engine raise an exception: %r' % e)
else:
self._internal_engine(session_data)
def _internal_engine(self, session_data):
"""Default internal processing engine: call default modbus func.
:type session_data: ModbusServer.SessionData
"""
try:
# call the ad-hoc function, if none exists, send an "illegal function" exception
func = self._func_map[session_data.request.pdu.func_code]
# check function found is callable
if not callable(func):
raise TypeError
# call ad-hoc func
func(session_data)
except (TypeError, KeyError):
session_data.response.pdu.build_except(session_data.request.pdu.func_code, EXP_ILLEGAL_FUNCTION)
def _read_bits(self, session_data):
"""
Functions Read Coils (0x01) or Read Discrete Inputs (0x02).
:param session_data: server engine data
:type session_data: ModbusServer.SessionData
"""
# pdu alias
recv_pdu = session_data.request.pdu
send_pdu = session_data.response.pdu
# decode pdu
(start_address, quantity_bits) = recv_pdu.unpack('>HH', from_byte=1, to_byte=5)
# check quantity of requested bits
if 0x0001 <= quantity_bits <= 0x07D0:
# data handler read request: for coils or discrete inputs space
if recv_pdu.func_code == READ_COILS:
ret_hdl = self.data_hdl.read_coils(start_address, quantity_bits, session_data.srv_info)
else:
ret_hdl = self.data_hdl.read_d_inputs(start_address, quantity_bits, session_data.srv_info)
# format regular or except response
if ret_hdl.ok:
# allocate bytes list
b_size = (quantity_bits + 7) // 8
bytes_l = [0] * b_size
# populate bytes list with data bank bits
for i, item in enumerate(ret_hdl.data):
if item:
bytes_l[i // 8] = set_bit(bytes_l[i // 8], i % 8)
# build pdu
send_pdu.add_pack('BB', recv_pdu.func_code, len(bytes_l))
send_pdu.add_pack('%dB' % len(bytes_l), *bytes_l)
else:
send_pdu.build_except(recv_pdu.func_code, ret_hdl.exp_code)
else:
send_pdu.build_except(recv_pdu.func_code, EXP_DATA_VALUE)
def _read_words(self, session_data):
"""
Functions Read Holding Registers (0x03) or Read Input Registers (0x04).
:param session_data: server engine data
:type session_data: ModbusServer.SessionData
"""
# pdu alias
recv_pdu = session_data.request.pdu
send_pdu = session_data.response.pdu
# decode pdu
(start_addr, quantity_regs) = recv_pdu.unpack('>HH', from_byte=1, to_byte=5)
# check quantity of requested words
if 0x0001 <= quantity_regs <= 0x007D:
# data handler read request: for holding or input registers space
if recv_pdu.func_code == READ_HOLDING_REGISTERS:
ret_hdl = self.data_hdl.read_h_regs(start_addr, quantity_regs, session_data.srv_info)
else:
ret_hdl = self.data_hdl.read_i_regs(start_addr, quantity_regs, session_data.srv_info)
# format regular or except response
if ret_hdl.ok:
# build pdu
send_pdu.add_pack('BB', recv_pdu.func_code, quantity_regs * 2)
# add_pack requested words
send_pdu.add_pack('>%dH' % len(ret_hdl.data), *ret_hdl.data)
else:
send_pdu.build_except(recv_pdu.func_code, ret_hdl.exp_code)
else:
send_pdu.build_except(recv_pdu.func_code, EXP_DATA_VALUE)
def _write_single_coil(self, session_data):
"""
Function Write Single Coil (0x05).
:param session_data: server engine data
:type session_data: ModbusServer.SessionData
"""
# pdu alias
recv_pdu = session_data.request.pdu
send_pdu = session_data.response.pdu
# decode pdu
(coil_addr, coil_value) = recv_pdu.unpack('>HH', from_byte=1, to_byte=5)
# format coil raw value to bool
coil_as_bool = bool(coil_value == 0xFF00)
# data handler update request
ret_hdl = self.data_hdl.write_coils(coil_addr, [coil_as_bool], session_data.srv_info)
# format regular or except response
if ret_hdl.ok:
send_pdu.add_pack('>BHH', recv_pdu.func_code, coil_addr, coil_value)
else:
send_pdu.build_except(recv_pdu.func_code, ret_hdl.exp_code)
def _write_single_register(self, session_data):
"""
Functions Write Single Register (0x06).
:param session_data: server engine data
:type session_data: ModbusServer.SessionData
"""
# pdu alias
recv_pdu = session_data.request.pdu
send_pdu = session_data.response.pdu
# decode pdu
(reg_addr, reg_value) = recv_pdu.unpack('>HH', from_byte=1, to_byte=5)
# data handler update request
ret_hdl = self.data_hdl.write_h_regs(reg_addr, [reg_value], session_data.srv_info)
# format regular or except response
if ret_hdl.ok:
send_pdu.add_pack('>BHH', recv_pdu.func_code, reg_addr, reg_value)
else:
send_pdu.build_except(recv_pdu.func_code, ret_hdl.exp_code)
def _write_multiple_coils(self, session_data):
"""
Function Write Multiple Coils (0x0F).
:param session_data: server engine data
:type session_data: ModbusServer.SessionData
"""
# pdu alias
recv_pdu = session_data.request.pdu
send_pdu = session_data.response.pdu
# decode pdu
(start_addr, quantity_bits, byte_count) = recv_pdu.unpack('>HHB', from_byte=1, to_byte=6)
# ok flags: some tests on pdu fields
qty_bits_ok = 0x0001 <= quantity_bits <= 0x07B0
b_count_ok = byte_count >= (quantity_bits + 7) // 8
pdu_len_ok = len(recv_pdu.raw[6:]) >= byte_count
# test ok flags
if qty_bits_ok and b_count_ok and pdu_len_ok:
# allocate bits list
bits_l = [False] * quantity_bits
# populate bits list with bits from rx frame
for i, _ in enumerate(bits_l):
bit_val = recv_pdu.raw[i // 8 + 6]
bits_l[i] = test_bit(bit_val, i % 8)
# data handler update request
ret_hdl = self.data_hdl.write_coils(start_addr, bits_l, session_data.srv_info)
# format regular or except response
if ret_hdl.ok:
send_pdu.add_pack('>BHH', recv_pdu.func_code, start_addr, quantity_bits)
else:
send_pdu.build_except(recv_pdu.func_code, ret_hdl.exp_code)
else:
send_pdu.build_except(recv_pdu.func_code, EXP_DATA_VALUE)
def _write_multiple_registers(self, session_data):
"""
Function Write Multiple Registers (0x10).
:param session_data: server engine data
:type session_data: ModbusServer.SessionData
"""
# pdu alias
recv_pdu = session_data.request.pdu
send_pdu = session_data.response.pdu
# decode pdu
(start_addr, quantity_regs, byte_count) = recv_pdu.unpack('>HHB', from_byte=1, to_byte=6)
# ok flags: some tests on pdu fields
qty_regs_ok = 0x0001 <= quantity_regs <= 0x007B
b_count_ok = byte_count == quantity_regs * 2
pdu_len_ok = len(recv_pdu.raw[6:]) >= byte_count
# test ok flags
if qty_regs_ok and b_count_ok and pdu_len_ok:
# allocate words list
regs_l = [0] * quantity_regs
# populate words list with words from rx frame
for i, _ in enumerate(regs_l):
offset = i * 2 + 6
regs_l[i] = recv_pdu.unpack('>H', from_byte=offset, to_byte=offset + 2)[0]
# data handler update request
ret_hdl = self.data_hdl.write_h_regs(start_addr, regs_l, session_data.srv_info)
# format regular or except response
if ret_hdl.ok:
send_pdu.add_pack('>BHH', recv_pdu.func_code, start_addr, quantity_regs)
else:
send_pdu.build_except(recv_pdu.func_code, ret_hdl.exp_code)
else:
send_pdu.build_except(recv_pdu.func_code, EXP_DATA_VALUE)
def _write_read_multiple_registers(self, session_data):
"""
Function Write Read Multiple Registers (0x17).
:param session_data: server engine data
:type session_data: ModbusServer.SessionData
"""
# pdu alias
recv_pdu = session_data.request.pdu
send_pdu = session_data.response.pdu
# decode pdu
(read_start_addr,
read_quantity_regs,
write_start_addr,
write_quantity_regs,
byte_count) = recv_pdu.unpack('>HHHHB', from_byte=1, to_byte=10)
# ok flags: some tests on pdu fields
write_qty_regs_ok = 0x0001 <= write_quantity_regs <= 0x007B
write_b_count_ok = byte_count == write_quantity_regs * 2
write_pdu_len_ok = len(recv_pdu.raw[10:]) >= byte_count
read_qty_regs_ok = 0x0001 <= read_quantity_regs <= 0x007B
# test ok flags
if write_qty_regs_ok and write_b_count_ok and write_pdu_len_ok and read_qty_regs_ok:
# allocate words list
regs_l = [0] * write_quantity_regs
# populate words list with words from rx frame
for i, _ in enumerate(regs_l):
offset = i * 2 + 10
regs_l[i] = recv_pdu.unpack('>H', from_byte=offset, to_byte=offset + 2)[0]
# data handler update request
ret_hdl = self.data_hdl.write_h_regs(write_start_addr, regs_l, session_data.srv_info)
# format regular or except response
if ret_hdl.ok:
ret_hdl = self.data_hdl.read_h_regs(read_start_addr, read_quantity_regs, session_data.srv_info)
if ret_hdl.ok:
# build pdu
send_pdu.add_pack('BB', recv_pdu.func_code, read_quantity_regs * 2)
# add_pack requested words
send_pdu.add_pack('>%dH' % len(ret_hdl.data), *ret_hdl.data)
else:
send_pdu.build_except(recv_pdu.func_code, ret_hdl.exp_code)
else:
send_pdu.build_except(recv_pdu.func_code, ret_hdl.exp_code)
else:
send_pdu.build_except(recv_pdu.func_code, EXP_DATA_VALUE)
def _encapsulated_interface_transport(self, session_data):
"""
Modbus Encapsulated Interface transport (MEI) endpoint (0x2B).
:param session_data: server engine data
:type session_data: ModbusServer.SessionData
"""
# pdu alias
recv_pdu = session_data.request.pdu
send_pdu = session_data.response.pdu
# decode pdu
(mei_type,) = recv_pdu.unpack('B', from_byte=1, to_byte=2)
# MEI type: read device identification
if mei_type == MEI_TYPE_READ_DEVICE_ID:
# check device_id property is set (default is None)
if not self.device_id:
# return except 2 if unset
send_pdu.build_except(recv_pdu.func_code, EXP_DATA_ADDRESS)
return
# list of requested objects
req_objects_l = list()
(device_id_code, object_id) = recv_pdu.unpack('BB', from_byte=2, to_byte=4)
# get basic device id (object id from 0x00 to 0x02)
if device_id_code == 1:
start_id = object_id
req_objects_l.extend(self.device_id.items(start=start_id, end=0x2))
# get regular device id (object id 0x03 to 0x7f)
elif device_id_code == 2:
start_id = max(object_id, 0x03)
req_objects_l.extend(self.device_id.items(start=start_id, end=0x7f))
# get extended device id (object id 0x80 to 0xff)
elif device_id_code == 3:
start_id = max(object_id, 0x80)
req_objects_l.extend(self.device_id.items(start=start_id, end=0xff))
# get specific id object
elif device_id_code == 4:
start_id = object_id
req_objects_l.extend(self.device_id.items(start=start_id, end=start_id))
else:
# return except 3 for unknown device id code
send_pdu.build_except(recv_pdu.func_code, EXP_DATA_VALUE)
return
# init variables for response PDU build
conformity_level = 0x83
more_follow = 0
next_obj_id = 0
number_of_objs = 0
fmt_pdu_head = 'BBBBBBB'
# format objects data part = [[obj id, obj len, obj val], ...]
obj_data_part = b''
for req_obj_id, req_obj_value in req_objects_l:
fmt_obj_blk = 'BB%ss' % len(req_obj_value)
# skip if the next add to data part will exceed max PDU size of modbus frame
if struct.calcsize(fmt_pdu_head) + len(obj_data_part) + struct.calcsize(fmt_obj_blk) > MAX_PDU_SIZE:
# turn on "more follow" field and set "next object id" field with next object id to ask
more_follow = 0xff
next_obj_id = req_obj_id
break
# ensure bytes type for object value
if isinstance(req_obj_value, str):
req_obj_value = req_obj_value.encode()
# add current object to data part
obj_data_part += struct.pack(fmt_obj_blk, req_obj_id, len(req_obj_value), req_obj_value)
number_of_objs += 1
# full PDU response = [PDU header] + [objects data part]
send_pdu.add_pack(fmt_pdu_head, recv_pdu.func_code, mei_type, device_id_code,
conformity_level, more_follow, next_obj_id, number_of_objs)
send_pdu.raw += obj_data_part
else:
# return except 2 for an unknown MEI type
send_pdu.build_except(recv_pdu.func_code, EXP_DATA_ADDRESS)
def start(self):
"""Start the server.
This function will block (or not if no_block flag is set).
"""
# do nothing if server is already running
if not self.is_run:
# set class attribute
ThreadingTCPServer.address_family = socket.AF_INET6 if self.ipv6 else socket.AF_INET
ThreadingTCPServer.daemon_threads = True
# init server
self._service = ThreadingTCPServer((self.host, self.port), self.ModbusService, bind_and_activate=False)
# pass some things shared with server threads (access via self.server in ModbusService.handle())
self._service.evt_running = self._evt_running
self._service.engine = self._engine
# set socket options
self._service.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self._service.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
# TODO test no_delay with bench
self._service.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
# bind and activate
try:
self._service.server_bind()
self._service.server_activate()
except OSError as e:
raise ModbusServer.NetworkError(e)
# serve request
if self.no_block:
self._serve_th = Thread(target=self._serve)
self._serve_th.daemon = True
self._serve_th.start()
else:
self._serve()
def stop(self):
"""Stop the server."""
if self.is_run:
self._service.shutdown()
self._service.server_close()
@property
def is_run(self):
"""Return True if server running.
"""
return self._evt_running.is_set()
def _serve(self):
try:
self._evt_running.set()
self._service.serve_forever()
except Exception:
self._service.server_close()
raise
except KeyboardInterrupt:
self._service.server_close()
finally:
self._evt_running.clear()
================================================
FILE: pyModbusTCP/utils.py
================================================
""" pyModbusTCP utils functions """
import re
import socket
import struct
###############
# bits function
###############
def get_bits_from_int(val_int, val_size=16):
"""Get the list of bits of val_int integer (default size is 16 bits).
Return bits list, the least significant bit first. Use list.reverse() for msb first.
:param val_int: integer value
:type val_int: int
:param val_size: bit length of integer (word = 16, long = 32) (optional)
:type val_size: int
:returns: list of boolean "bits" (the least significant first)
:rtype: list
"""
bits = []
# populate bits list with bool items of val_int
for i in range(val_size):
bits.append(bool((val_int >> i) & 0x01))
# return bits list
return bits
# short alias
int2bits = get_bits_from_int
def byte_length(bit_length):
"""Return the number of bytes needs to contain a bit_length structure.
:param bit_length: the number of bits
:type bit_length: int
:returns: the number of bytes
:rtype: int
"""
return (bit_length + 7) // 8
def test_bit(value, offset):
"""Test a bit at offset position.
:param value: value of integer to test
:type value: int
:param offset: bit offset (0 is lsb)
:type offset: int
:returns: value of bit at offset position
:rtype: bool
"""
mask = 1 << offset
return bool(value & mask)
def set_bit(value, offset):
"""Set a bit at offset position.
:param value: value of integer where set the bit
:type value: int
:param offset: bit offset (0 is lsb)
:type offset: int
:returns: value of integer with bit set
:rtype: int
"""
mask = 1 << offset
return int(value | mask)
def reset_bit(value, offset):
"""Reset a bit at offset position.
:param value: value of integer where reset the bit
:type value: int
:param offset: bit offset (0 is lsb)
:type offset: int
:returns: value of integer with bit reset
:rtype: int
"""
mask = ~(1 << offset)
return int(value & mask)
def toggle_bit(value, offset):
"""Return an integer with the bit at offset position inverted.
:param value: value of integer where invert the bit
:type value: int
:param offset: bit offset (0 is lsb)
:type offset: int
:returns: value of integer with bit inverted
:rtype: int
"""
mask = 1 << offset
return int(value ^ mask)
########################
# Word convert functions
########################
def word_list_to_long(val_list, big_endian=True, long_long=False):
"""Word list (16 bits) to long (32 bits) or long long (64 bits) list.
By default, word_list_to_long() use big endian order. For use little endian, set
big_endian param to False. Output format could be long long with long_long.
option set to True.
:param val_list: list of 16 bits int value
:type val_list: list
:param big_endian: True for big endian/False for little (optional)
:type big_endian: bool
:param long_long: True for long long 64 bits, default is long 32 bits (optional)
:type long_long: bool
:returns: list of 32 bits int value
:rtype: list
"""
long_list = []
block_size = 4 if long_long else 2
# populate long_list (len is half or quarter of 16 bits val_list) with 32 or 64 bits value
for index in range(int(len(val_list) / block_size)):
start = block_size * index
long = 0
if big_endian:
if long_long:
long += (val_list[start] << 48) + (val_list[start + 1] << 32)
long += (val_list[start + 2] << 16) + (val_list[start + 3])
else:
long += (val_list[start] << 16) + val_list[start + 1]
else:
if long_long:
long += (val_list[start + 3] << 48) + (val_list[start + 2] << 32)
long += (val_list[start + 1] << 16) + val_list[start]
long_list.append(long)
# return long list
return long_list
# short alias
words2longs = word_list_to_long
def long_list_to_word(val_list, big_endian=True, long_long=False):
"""Long (32 bits) or long long (64 bits) list to word (16 bits) list.
By default long_list_to_word() use big endian order. For use little endian, set
big_endian param to False. Input format could be long long with long_long
param to True.
:param val_list: list of 32 bits int value
:type val_list: list
:param big_endian: True for big endian/False for little (optional)
:type big_endian: bool
:param long_long: True for long long 64 bits, default is long 32 bits (optional)
:type long_long: bool
:returns: list of 16 bits int value
:rtype: list
"""
word_list = []
# populate 16 bits word_list with 32 or 64 bits value of val_list
for val in val_list:
block_l = [val & 0xffff, (val >> 16) & 0xffff]
if long_long:
block_l.append((val >> 32) & 0xffff)
block_l.append((val >> 48) & 0xffff)
if big_endian:
block_l.reverse()
word_list.extend(block_l)
# return long list
return word_list
# short alias
longs2words = long_list_to_word
##########################
# 2's complement functions
##########################
def get_2comp(val_int, val_size=16):
"""Get the 2's complement of Python int val_int.
:param val_int: int value to apply 2's complement
:type val_int: int
:param val_size: bit size of int value (word = 16, long = 32) (optional)
:type val_size: int
:returns: 2's complement result
:rtype: int
:raises ValueError: if mismatch between val_int and val_size
"""
# avoid overflow
if not (-1 << val_size - 1) <= val_int < (1 << val_size):
err_msg = 'could not compute two\'s complement for %i on %i bits'
err_msg %= (val_int, val_size)
raise ValueError(err_msg)
# test negative int
if val_int < 0:
val_int += 1 << val_size
# test MSB (do two's comp if set)
elif val_int & (1 << (val_size - 1)):
val_int -= 1 << val_size
return val_int
# short alias
twos_c = get_2comp
def get_list_2comp(val_list, val_size=16):
"""Get the 2's complement of Python list val_list.
:param val_list: list of int value to apply 2's complement
:type val_list: list
:param val_size: bit size of int value (word = 16, long = 32) (optional)
:type val_size: int
:returns: 2's complement result
:rtype: list
"""
return [get_2comp(val, val_size) for val in val_list]
# short alias
twos_c_l = get_list_2comp
###############################
# IEEE floating-point functions
###############################
def decode_ieee(val_int, double=False):
"""Decode Python int (32 bits integer) as an IEEE single or double precision format.
Support NaN.
:param val_int: a 32 or 64 bits integer as an int Python value
:type val_int: int
:param double: set to decode as a 64 bits double precision,
default is 32 bits single (optional)
:type double: bool
:returns: float result
:rtype: float
"""
if double:
return struct.unpack("d", struct.pack("Q", val_int))[0]
else:
return struct.unpack("f", struct.pack("I", val_int))[0]
def encode_ieee(val_float, double=False):
"""Encode Python float to int (32 bits integer) as an IEEE single or double precision format.
Support NaN.
:param val_float: float value to convert
:type val_float: float
:param double: set to encode as a 64 bits double precision,
default is 32 bits single (optional)
:type double: bool
:returns: IEEE 32 bits (single precision) as Python int
:rtype: int
"""
if double:
return struct.unpack("Q", struct.pack("d", val_float))[0]
else:
return struct.unpack("I", struct.pack("f", val_float))[0]
################
# misc functions
################
def crc16(frame):
"""Compute CRC16.
:param frame: frame
:type frame: bytes
:returns: CRC16
:rtype: int
"""
crc = 0xFFFF
for item in frame:
next_byte = item
crc ^= next_byte
for _ in range(8):
lsb = crc & 1
crc >>= 1
if lsb:
crc ^= 0xA001
return crc
def valid_host(host_str):
"""Validate a host string.
Can be an IPv4/6 address or a valid hostname.
:param host_str: the host string to test
:type host_str: str
:returns: True if host_str is valid
:rtype: bool
"""
# IPv4 valid address ?
try:
socket.inet_pton(socket.AF_INET, host_str)
return True
except socket.error:
pass
# IPv6 valid address ?
try:
socket.inet_pton(socket.AF_INET6, host_str)
return True
except socket.error:
pass
# valid hostname ?
if len(host_str) > 255:
return False
# strip final dot, if present
if host_str[-1] == '.':
host_str = host_str[:-1]
# validate each part of the hostname (part_1.part_2.part_3)
re_part_ok = re.compile('(?!-)[a-z0-9-_]{1,63}(?<!-)$', re.IGNORECASE)
return all(re_part_ok.match(part) for part in host_str.split('.'))
================================================
FILE: setup.cfg
================================================
[build_sphinx]
source-dir = docs/
all_files = 1
================================================
FILE: setup.py
================================================
from pyModbusTCP import constants
from setuptools import setup
with open('README.rst') as f:
readme = f.read()
setup(
name="pyModbusTCP",
version=constants.VERSION,
description="A simple Modbus/TCP library for Python",
long_description=readme,
author="Loic Lefebvre",
author_email="loic.celine@free.fr",
license="MIT",
url="https://github.com/sourceperl/pyModbusTCP",
packages=["pyModbusTCP"],
platforms="any",
)
================================================
FILE: tests/test_client.py
================================================
""" Test of pyModbusTCP.ModbusClient """
import unittest
from pyModbusTCP.client import ModbusClient
class TestModbusClient(unittest.TestCase):
""" ModbusClient tests class. """
def test_host(self):
"""Test of host property."""
# default value
self.assertEqual(ModbusClient().host, 'localhost')
# should raise ValueError for bad value
self.assertRaises(ValueError, ModbusClient, host='wrong@host')
self.assertRaises(ValueError, ModbusClient, host='::notip:1')
# shouldn't raise ValueError for valid value
try:
[ModbusClient(host=h) for h in ['CamelCaseHost', 'plc-1.net', 'my.good.host',
'_test.example.com', '42.example.com',
'127.0.0.1', '::1']]
except ValueError:
self.fail('ModbusClient.host property raised ValueError unexpectedly')
def test_port(self):
"""Test of port property."""
# default value
self.assertEqual(ModbusClient().port, 502)
# should raise an exception for bad value
self.assertRaises(TypeError, ModbusClient, port='amsterdam')
self.assertRaises(ValueError, ModbusClient, port=-1)
# shouldn't raise ValueError for valid value
try:
ModbusClient(port=5020)
except ValueError:
self.fail('ModbusClient.port property raised ValueError unexpectedly')
def test_unit_id(self):
"""Test of unit_id property."""
# default value
self.assertEqual(ModbusClient().unit_id, 1)
# should raise an exception for bad unit_id
self.assertRaises(TypeError, ModbusClient, unit_id='@')
self.assertRaises(ValueError, ModbusClient, unit_id=420)
# shouldn't raise ValueError for valid value
try:
ModbusClient(port=5020)
except ValueError:
self.fail('ModbusClient.port property raised ValueError unexpectedly')
def test_misc(self):
"""Check of misc default values."""
self.assertEqual(ModbusClient().auto_open, True)
self.assertEqual(ModbusClient().auto_close, False)
if __name__ == '__main__':
unittest.main()
================================================
FILE: tests/test_client_server.py
================================================
""" Test of pyModbusTCP client-server interaction """
import unittest
from random import randint, getrandbits, choice
from string import ascii_letters
from pyModbusTCP.server import ModbusServer, DeviceIdentification
from pyModbusTCP.client import ModbusClient, DeviceIdentificationResponse
from pyModbusTCP.constants import SUPPORTED_FUNCTION_CODES, \
EXP_NONE, EXP_ILLEGAL_FUNCTION, EXP_DATA_ADDRESS, EXP_DATA_VALUE, MB_NO_ERR, MB_EXCEPT_ERR
# some const
MAX_READABLE_REGS = 125
MAX_WRITABLE_REGS = 123
MAX_WRITE_READ_REGS = 121
MAX_READABLE_BITS = 2000
MAX_WRITABLE_BITS = 1968
class TestClientServer(unittest.TestCase):
""" Client-server interaction test class. """
def setUp(self):
"""Init client-server for test_xxx methods."""
# modbus server
self.server = ModbusServer(port=5020, no_block=True)
self.server.start()
# modbus client
self.client = ModbusClient(port=5020)
self.client.open()
def tearDown(self):
"""Cleanning after test."""
self.client.close()
self.server.stop()
def test_default_startup_values(self):
"""Some read at random address to test startup values."""
for addr in [randint(0, 0xffff) for _ in range(100)]:
self.assertEqual(self.client.read_coils(addr), [False])
self.assertEqual(self.client.read_discrete_inputs(addr), [False])
self.assertEqual(self.client.read_holding_registers(addr), [0])
self.assertEqual(self.client.read_input_registers(addr), [0])
def test_read_write_requests(self):
"""Test standard modbus functions."""
# coils
for addr in [0x0000, 0x1234, 0x2345, 0x10000 - MAX_WRITABLE_BITS]:
# coils space: single read/write
bit = bool(getrandbits(1))
self.assertEqual(self.client.write_single_coil(addr, bit), True)
self.assertEqual(self.client.read_coils(addr), [bit])
# coils space: multiple read/write at min size
bits_l = [bool(getrandbits(1))]
self.assertEqual(self.client.write_multiple_coils(addr, bits_l), True)
self.assertEqual(self.client.read_coils(addr, len(bits_l)), bits_l)
# coils space: multiple read/write at max size
bits_l = [bool(getrandbits(1)) for _ in range(MAX_WRITABLE_BITS)]
self.assertEqual(self.client.write_multiple_coils(addr, bits_l), True)
self.assertEqual(self.client.read_coils(addr, len(bits_l)), bits_l)
# coils space: oversized multi-write
bits_l.append(bool(getrandbits(1)))
self.assertRaises(ValueError, self.client.write_multiple_coils, addr, bits_l)
# coils space: read/write over limit
self.assertRaises(ValueError, self.client.read_coils, 0xfffe, 3)
self.assertRaises(ValueError, self.client.write_single_coil, 0x10000, False)
self.assertRaises(ValueError, self.client.write_multiple_coils, 0xfff0, [False] * 17)
# discrete inputs
for addr in [0x0000, 0x1234, 0x2345, 0x10000 - MAX_READABLE_BITS]:
# discrete inputs space: single read/write
bit = bool(getrandbits(1))
self.server.data_bank.set_discrete_inputs(addr, [bit])
self.assertEqual(self.client.read_discrete_inputs(addr), [bit])
# discrete inputs space: multiple read/write at min size
bits_l = [bool(getrandbits(1))]
self.server.data_bank.set_discrete_inputs(addr, bits_l)
self.assertEqual(self.client.read_discrete_inputs(addr, len(bits_l)), bits_l)
# discrete inputs space: multiple read/write at max size
bits_l = [bool(getrandbits(1)) for _ in range(MAX_READABLE_BITS)]
self.server.data_bank.set_discrete_inputs(addr, bits_l)
self.assertEqual(self.client.read_discrete_inputs(addr, len(bits_l)), bits_l)
# discrete inputs space: multiple read/write at max size
bits_l.append(bool(getrandbits(1)))
self.server.data_bank.set_discrete_inputs(addr, bits_l)
self.assertRaises(ValueError, self.client.read_discrete_inputs, addr, len(bits_l))
# discrete inputs space: read/write over limit
self.assertRaises(ValueError, self.client.read_discrete_inputs, 0xffff, 2)
# holding registers
for addr in [0x0000, 0x1234, 0x2345, 0x10000 - MAX_WRITABLE_REGS]:
# holding registers space: single read/write
word = randint(0, 0xffff)
self.assertEqual(self.client.write_single_register(addr, word), True)
self.assertEqual(self.client.read_holding_registers(addr), [word])
# holding registers space: multi-write at max size
words_l = [randint(0, 0xffff) for _ in range(MAX_WRITABLE_REGS)]
self.assertEqual(self.client.write_multiple_registers(addr, words_l), True)
self.assertEqual(self.client.read_holding_registers(addr, len(words_l)), words_l)
# holding registers space: multi-write at max size
words_l = [randint(0, 0xffff) for _ in range(MAX_WRITE_READ_REGS)]
self.assertEqual(self.client.write_read_multiple_registers(addr, words_l, addr, len(words_l)), words_l)
self.assertEqual(self.client.read_holding_registers(addr, len(words_l)), words_l)
# holding registers space: read/write over limit
self.assertRaises(ValueError, self.client.read_holding_registers, 0xfff0, 17)
self.assertRaises(ValueError, self.client.write_single_register, 0, 0x10000)
self.assertRaises(ValueError, self.client.write_single_register, 0x10000, 0)
self.assertRaises(ValueError, self.client.write_multiple_registers, 0x1000, [0x10000])
self.assertRaises(ValueError, self.client.write_multiple_registers, 0xfff0, [0] * 17)
self.assertRaises(ValueError, self.client.write_read_multiple_registers, 0x1000, [0x10000], 0x1000, 1)
self.assertRaises(ValueError, self.client.write_read_multiple_registers, 0xfff0, [0] * 17, 0xfff0, 1)
self.assertRaises(ValueError, self.client.write_read_multiple_registers, 0xfff0, [0] * 1, 0xfff0, 17)
# input registers
for addr in [0x0000, 0x1234, 0x2345, 0x10000 - MAX_READABLE_REGS]:
# input registers space: single read/write
word = randint(0, 0xffff)
self.server.data_bank.set_input_registers(addr, [word])
self.assertEqual(self.client.read_input_registers(addr), [word])
# input registers space: multiple read/write at max size
words_l = [randint(0, 0xffff) for _ in range(MAX_READABLE_REGS)]
self.server.data_bank.set_input_registers(addr, words_l)
self.assertEqual(self.client.read_input_registers(addr, len(words_l)), words_l)
# input registers space: multiple read/write over sized
words_l.append(randint(0, 0xffff))
self.server.data_bank.set_input_registers(addr, words_l)
self.assertRaises(ValueError, self.client.read_input_registers, addr, len(words_l))
# input registers space: read/write over limit
self.assertRaises(ValueError, self.client.read_input_registers, 0xfff0, 17)
def test_server_strength(self):
"""Test server responses to abnormal events."""
# unsupported function codes must return except EXP_ILLEGAL_FUNCTION
for func_code in range(0x80):
if func_code not in SUPPORTED_FUNCTION_CODES:
# test with a min PDU length of 2 bytes (avoid short frame error)
self.assertEqual(self.client.custom_request(bytes([func_code, 0x00])), None)
self.assertEqual(self.client.last_error, MB_EXCEPT_ERR)
self.assertEqual(self.client.last_except, EXP_ILLEGAL_FUNCTION)
# check a regular request status: no error, no except
self.assertEqual(self.client.read_coils(0), [False])
self.assertEqual(self.client.last_error, MB_NO_ERR)
self.assertEqual(self.client.last_except, EXP_NONE)
def test_server_read_identification(self):
"""Test server device indentification function."""
# forge a basic read identification on unconfigured server (return a data address except)
self.assertEqual(self.client.custom_request(b'\x2b\x0e\x01\x00'), None)
self.assertEqual(self.client.last_error, MB_EXCEPT_ERR)
self.assertEqual(self.client.last_except, EXP_DATA_ADDRESS)
# configure server
self.server.device_id = DeviceIdentification()
self.server.device_id.vendor_name = b'me'
self.server.device_id[0x80] = b'\xc0\xde'
# forge a basic read identification on a configured server (return a valid pdu)
self.assertNotEqual(self.client.custom_request(b'\x2b\x0e\x01\x00'), None)
# forge a read identifaction request with a bad read device id code (return except 3)
self.assertEqual(self.client.custom_request(b'\x2b\x0e\x05\x00'), None)
self.assertEqual(self.client.last_error, MB_EXCEPT_ERR)
self.assertEqual(self.client.last_except, EXP_DATA_VALUE)
# read VendorName str object(id #0) with individual access (read device id = 4)
ret_pdu = self.client.custom_request(b'\x2b\x0e\x04\x00')
self.assertEqual(ret_pdu, b'\x2b\x0e\x04\x83\x00\x00\x01\x00\x02me')
# read private int object(id #0x80) with individual access (read device id = 4)
ret_pdu = self.client.custom_request(b'\x2b\x0e\x04\x80')
self.assertEqual(ret_pdu, b'\x2b\x0e\x04\x83\x00\x00\x01\x80\x02\xc0\xde')
# restore default configuration
self.server.device_id = None
def test_client_read_identification(self):
"""Test client device indentification function."""
# configure server
vendor_name = ''.join(choice(ascii_letters) for _ in range(16)).encode()
product_code = ''.join(choice(ascii_letters) for _ in range(32)).encode()
maj_min_rev = b'v2.0'
vendor_url = b'https://github.com/sourceperl/pyModbusTCP'
self.server.device_id = DeviceIdentification(vendor_name=vendor_name, product_code=product_code,
major_minor_revision=maj_min_rev, vendor_url=vendor_url)
# read_device_identification: read basic device identification (stream access)
dev_id_resp = self.client.read_device_identification()
if not dev_id_resp:
self.fail('ModbusClient.read_device_identification() method failed unexpectedly')
else:
# return DeviceIdentificationResponse on success
self.assertEqual(isinstance(dev_id_resp, DeviceIdentificationResponse), True)
# check read data
self.assertEqual(len(dev_id_resp.objects_by_id), 3)
self.assertEqual(dev_id_resp.vendor_name, vendor_name)
self.assertEqual(dev_id_resp.objects_by_id.get(0), vendor_name)
self.assertEqual(dev_id_resp.product_code, product_code)
self.assertEqual(dev_id_resp.objects_by_id.get(1), product_code)
self.assertEqual(dev_id_resp.major_minor_revision, maj_min_rev)
self.assertEqual(dev_id_resp.objects_by_id.get(2), maj_min_rev)
self.assertEqual(dev_id_resp.vendor_url, None)
self.assertEqual(dev_id_resp.product_name, None)
self.assertEqual(dev_id_resp.model_name, None)
self.assertEqual(dev_id_resp.user_application_name, None)
# read_device_identification: read one specific identification object (individual access)
dev_id_resp = self.client.read_device_identification(read_code=4, object_id=3)
if not dev_id_resp:
self.fail('ModbusClient.read_device_identification() method failed unexpectedly')
else:
# return DeviceIdentificationResponse on success
self.assertEqual(isinstance(dev_id_resp, DeviceIdentificationResponse), True)
# check read data
self.assertEqual(len(dev_id_resp.objects_by_id), 1)
self.assertEqual(dev_id_resp.vendor_url, vendor_url)
self.assertEqual(dev_id_resp.objects_by_id.get(3), vendor_url)
# restore default configuration
self.server.device_id = None
if __name__ == '__main__':
unittest.main()
================================================
FILE: tests/test_server.py
================================================
""" Test of pyModbusTCP.ModbusServer """
import unittest
from pyModbusTCP.server import ModbusServer, DeviceIdentification
class TestModbusServer(unittest.TestCase):
""" ModbusServer tests class. """
def test_device_identification(self):
"""Some tests around modbus device identification."""
# should raise exception
self.assertRaises(TypeError, ModbusServer, device_id=object())
# shouldn't raise exception
try:
ModbusServer(device_id=DeviceIdentification())
except Exception as e:
self.fail('ModbusServer raised exception "%r" unexpectedly' % e)
# init a DeviceIdentification class for test it
device_id = DeviceIdentification()
# should raise exception
with self.assertRaises(TypeError):
device_id['obj_name'] = 'anything'
with self.assertRaises(TypeError):
device_id[0] = 42
# shouldn't raise exception
try:
device_id.vendor_name = b'me'
device_id.user_application_name = b'unittest'
device_id[0x80] = b'feed'
except Exception as e:
self.fail('DeviceIdentification raised exception "%r" unexpectedly' % e)
# check access by shortcut name (str) or object id (int) return same value
self.assertEqual(device_id.vendor_name, device_id[0x00])
self.assertEqual(device_id.user_application_name, device_id[0x06])
# test __repr__
device_id = DeviceIdentification(
product_name=b'server', objects_id={42: b'this'})
self.assertEqual(repr(device_id), "DeviceIdentification(product_name=b'server', objects_id={42: b'this'})")
if __name__ == '__main__':
unittest.main()
================================================
FILE: tests/test_utils.py
================================================
""" Test of pyModbusTCP.utils """
import unittest
import math
from pyModbusTCP.utils import \
get_bits_from_int, int2bits, decode_ieee, encode_ieee, \
word_list_to_long, words2longs, long_list_to_word, longs2words, \
get_2comp, twos_c, get_list_2comp, twos_c_l
class TestUtils(unittest.TestCase):
""" pyModbusTCP.utils function test class. """
def test_get_bits_from_int(self):
"""Test function get_bits_from_int and it's short alias int2bits."""
# default bits list size is 16
self.assertEqual(len(get_bits_from_int(0)), 16)
# for 8 size (positional arg)
self.assertEqual(len(get_bits_from_int(0, 8)), 8)
# for 32 size (named arg)
self.assertEqual(len(get_bits_from_int(0, val_size=32)), 32)
# test binary decode
self.assertEqual(int2bits(0x0000), [False]*16)
self.assertEqual(int2bits(0xffff), [True]*16)
self.assertEqual(int2bits(0xf007), [True]*3 + [False]*9 + [True]*4)
self.assertEqual(int2bits(6, 4), [False, True, True, False])
def test_ieee(self):
"""Test IEEE functions: decode_ieee and encode_ieee."""
# test IEEE NaN
self.assertTrue(math.isnan(decode_ieee(0x7fc00000)))
self.assertEqual(encode_ieee(float('nan')), 0x7fc00000)
# test +/- infinity
self.assertTrue(math.isinf(decode_ieee(0xff800000)))
self.assertTrue(math.isinf(decode_ieee(0x7f800000)))
# test big and small values
avogad = 6.022140857e+23
avo_32 = 0x66ff0c2f
avo_64 = 0x44dfe185d2f54b67
planck = 6.62606957e-34
pla_32 = 0x085c305e
pla_64 = 0x390b860bb596a559
# IEEE single or double precision format -> float
self.assertAlmostEqual(decode_ieee(avo_32), avogad, delta=avogad*1e-7)
self.assertAlmostEqual(decode_ieee(avo_64, double=True), avogad)
self.assertAlmostEqual(decode_ieee(pla_32), planck)
self.assertAlmostEqual(decode_ieee(pla_64, double=True), planck)
# float -> IEEE single or double precision format
self.assertAlmostEqual(encode_ieee(avogad), avo_32)
self.assertAlmostEqual(enc
gitextract_aihtbohh/
├── .github/
│ └── workflows/
│ └── tests.yml
├── .gitignore
├── .readthedocs.yaml
├── CHANGES
├── HOWTO-PyPi.md
├── HOWTO-pkg-devel.md
├── LICENSE
├── MANIFEST.in
├── README.rst
├── docs/
│ ├── conf.py
│ ├── examples/
│ │ ├── client_float.rst
│ │ ├── client_minimal.rst
│ │ ├── client_read_coils.rst
│ │ ├── client_read_h_registers.rst
│ │ ├── client_thread.rst
│ │ ├── client_write_coils.rst
│ │ ├── index.rst
│ │ ├── server.rst
│ │ ├── server_allow.rst
│ │ ├── server_change_log.rst
│ │ ├── server_schedule.rst
│ │ ├── server_serial_gw.rst
│ │ └── server_virtual_data.rst
│ ├── index.rst
│ ├── package/
│ │ ├── class_ModbusClient.rst
│ │ ├── class_ModbusServer.rst
│ │ ├── index.rst
│ │ └── module_utils.rst
│ ├── quickstart/
│ │ ├── index.rst
│ │ └── map.dot
│ └── requirements.txt
├── examples/
│ ├── README.md
│ ├── client_debug.py
│ ├── client_float.py
│ ├── client_minimal.py
│ ├── client_read_coils.py
│ ├── client_read_h_registers.py
│ ├── client_serial_gw.py
│ ├── client_thread.py
│ ├── client_write_coils.py
│ ├── server.py
│ ├── server_allow.py
│ ├── server_change_log.py
│ ├── server_schedule.py
│ ├── server_serial_gw.py
│ └── server_virtual_data.py
├── pyModbusTCP/
│ ├── __init__.py
│ ├── client.py
│ ├── constants.py
│ ├── server.py
│ └── utils.py
├── setup.cfg
├── setup.py
└── tests/
├── test_client.py
├── test_client_server.py
├── test_server.py
└── test_utils.py
SYMBOL INDEX (250 symbols across 15 files)
FILE: examples/client_float.py
class FloatModbusClient (line 10) | class FloatModbusClient(ModbusClient):
method read_float (line 13) | def read_float(self, address, number=1):
method write_float (line 21) | def write_float(self, address, floats_list):
FILE: examples/client_serial_gw.py
class ModbusRTUFrame (line 26) | class ModbusRTUFrame:
method __init__ (line 29) | def __init__(self, raw=b''):
method __repr__ (line 33) | def __repr__(self) -> str:
method as_hex (line 37) | def as_hex(self) -> str:
method pdu (line 42) | def pdu(self):
method slave_addr (line 47) | def slave_addr(self):
method function_code (line 52) | def function_code(self):
method is_set (line 57) | def is_set(self):
method is_valid (line 66) | def is_valid(self):
method build (line 74) | def build(self, raw_pdu, slave_addr):
class SlaveSerialWorker (line 89) | class SlaveSerialWorker:
method __init__ (line 92) | def __init__(self, port, end_of_frame=0.05):
method handle_request (line 99) | def handle_request(self):
method run (line 103) | def run(self):
class Serial2ModbusClient (line 143) | class Serial2ModbusClient:
method __init__ (line 146) | def __init__(self, serial_w, mbus_cli, slave_addr=1, allow_bcast=False):
method _handle_request (line 166) | def _handle_request(self):
method run (line 185) | def run(self):
FILE: examples/client_thread.py
function polling_thread (line 26) | def polling_thread():
FILE: examples/server_allow.py
class MyDataHandler (line 21) | class MyDataHandler(DataHandler):
method read_coils (line 22) | def read_coils(self, address, count, srv_info):
method read_d_inputs (line 28) | def read_d_inputs(self, address, count, srv_info):
method read_h_regs (line 34) | def read_h_regs(self, address, count, srv_info):
method read_i_regs (line 40) | def read_i_regs(self, address, count, srv_info):
method write_coils (line 46) | def write_coils(self, address, bits_l, srv_info):
method write_h_regs (line 52) | def write_h_regs(self, address, words_l, srv_info):
FILE: examples/server_change_log.py
class MyDataBank (line 15) | class MyDataBank(DataBank):
method on_coils_change (line 18) | def on_coils_change(self, address, from_value, to_value, srv_info):
method on_holding_registers_change (line 24) | def on_holding_registers_change(self, address, from_value, to_value, s...
FILE: examples/server_schedule.py
function alive_word_job (line 24) | def alive_word_job():
FILE: examples/server_serial_gw.py
class ModbusRTUFrame (line 32) | class ModbusRTUFrame:
method __init__ (line 35) | def __init__(self, raw=b''):
method pdu (line 40) | def pdu(self):
method slave_address (line 45) | def slave_address(self):
method function_code (line 50) | def function_code(self):
method is_valid (line 55) | def is_valid(self):
method build (line 63) | def build(self, raw_pdu, slave_ad):
class RtuQuery (line 78) | class RtuQuery:
method __init__ (line 81) | def __init__(self):
class ModbusSerialWorker (line 87) | class ModbusSerialWorker:
method __init__ (line 90) | def __init__(self, port, timeout=1.0, end_of_frame=0.05):
method loop (line 99) | def loop(self):
method srv_engine_entry (line 126) | def srv_engine_entry(self, session_data):
FILE: examples/server_virtual_data.py
class MyDataBank (line 20) | class MyDataBank(DataBank):
method __init__ (line 23) | def __init__(self):
method get_holding_registers (line 28) | def get_holding_registers(self, address, number=1, srv_info=None):
FILE: pyModbusTCP/client.py
class DeviceIdentificationResponse (line 29) | class DeviceIdentificationResponse:
method vendor_name (line 47) | def vendor_name(self):
method product_code (line 51) | def product_code(self):
method major_minor_revision (line 55) | def major_minor_revision(self):
method vendor_url (line 59) | def vendor_url(self):
method product_name (line 63) | def product_name(self):
method model_name (line 67) | def model_name(self):
method user_application_name (line 71) | def user_application_name(self):
class ModbusClient (line 75) | class ModbusClient:
class _InternalError (line 78) | class _InternalError(Exception):
class _NetworkError (line 81) | class _NetworkError(_InternalError):
method __init__ (line 82) | def __init__(self, code, message):
class _ModbusExcept (line 86) | class _ModbusExcept(_InternalError):
method __init__ (line 87) | def __init__(self, code):
method __init__ (line 90) | def __init__(self, host='localhost', port=502, unit_id=1, timeout=30.0...
method __repr__ (line 130) | def __repr__(self):
method __del__ (line 135) | def __del__(self):
method version (line 139) | def version(self):
method last_error (line 144) | def last_error(self):
method last_error_as_txt (line 149) | def last_error_as_txt(self):
method last_except (line 154) | def last_except(self):
method last_except_as_txt (line 159) | def last_except_as_txt(self):
method last_except_as_full_txt (line 165) | def last_except_as_full_txt(self):
method host (line 171) | def host(self):
method host (line 180) | def host(self, value):
method port (line 194) | def port(self):
method port (line 202) | def port(self, value):
method unit_id (line 216) | def unit_id(self):
method unit_id (line 224) | def unit_id(self, value):
method timeout (line 236) | def timeout(self):
method timeout (line 245) | def timeout(self, value):
method auto_open (line 258) | def auto_open(self):
method auto_open (line 263) | def auto_open(self, value):
method auto_close (line 268) | def auto_close(self):
method auto_close (line 273) | def auto_close(self, value):
method is_open (line 278) | def is_open(self):
method open (line 282) | def open(self):
method _open (line 295) | def _open(self):
method close (line 322) | def close(self):
method custom_request (line 326) | def custom_request(self, pdu):
method read_coils (line 342) | def read_coils(self, bit_addr, bit_nb=1):
method read_discrete_inputs (line 382) | def read_discrete_inputs(self, bit_addr, bit_nb=1):
method read_holding_registers (line 422) | def read_holding_registers(self, reg_addr, reg_nb=1):
method read_input_registers (line 462) | def read_input_registers(self, reg_addr, reg_nb=1):
method read_device_identification (line 502) | def read_device_identification(self, read_code=1, object_id=0):
method write_single_coil (line 556) | def write_single_coil(self, bit_addr, bit_value):
method write_single_register (line 587) | def write_single_register(self, reg_addr, reg_value):
method write_multiple_coils (line 618) | def write_multiple_coils(self, bits_addr, bits_value):
method write_multiple_registers (line 661) | def write_multiple_registers(self, regs_addr, regs_value):
method write_read_multiple_registers (line 705) | def write_read_multiple_registers(self, write_addr, write_values, read...
method _send (line 767) | def _send(self, frame):
method _send_pdu (line 786) | def _send_pdu(self, pdu):
method _recv (line 802) | def _recv(self, size):
method _recv_all (line 823) | def _recv_all(self, size):
method _recv_pdu (line 836) | def _recv_pdu(self, min_len=2):
method _add_mbap (line 881) | def _add_mbap(self, pdu):
method _req_pdu (line 897) | def _req_pdu(self, tx_pdu, rx_min_len=2):
method _req_init (line 914) | def _req_init(self):
method _req_except_handler (line 919) | def _req_except_handler(self, _except):
method _debug_msg (line 931) | def _debug_msg(self, msg: str):
method _on_tx_rx (line 934) | def _on_tx_rx(self, frame: bytes, is_tx: bool):
method on_tx_rx (line 944) | def on_tx_rx(self, frame: bytes, is_tx: bool):
FILE: pyModbusTCP/server.py
class DataBank (line 24) | class DataBank:
method get_bits (line 30) | def get_bits(cls, *_args, **_kwargs):
method set_bits (line 35) | def set_bits(cls, *_args, **_kwargs):
method get_words (line 40) | def get_words(cls, *_args, **_kwargs):
method set_words (line 45) | def set_words(cls, *_args, **_kwargs):
method __init__ (line 49) | def __init__(self, coils_size=0x10000, coils_default_value=False,
method __repr__ (line 103) | def __repr__(self):
method get_coils (line 112) | def get_coils(self, address, number=1, srv_info=None):
method set_coils (line 131) | def set_coils(self, address, bit_list, srv_info=None):
method get_discrete_inputs (line 165) | def get_discrete_inputs(self, address, number=1, srv_info=None):
method set_discrete_inputs (line 184) | def set_discrete_inputs(self, address, bit_list):
method get_holding_registers (line 206) | def get_holding_registers(self, address, number=1, srv_info=None):
method set_holding_registers (line 225) | def set_holding_registers(self, address, word_list, srv_info=None):
method get_input_registers (line 259) | def get_input_registers(self, address, number=1, srv_info=None):
method set_input_registers (line 278) | def set_input_registers(self, address, word_list):
method on_coils_change (line 302) | def on_coils_change(self, address, from_value, to_value, srv_info):
method on_holding_registers_change (line 318) | def on_holding_registers_change(self, address, from_value, to_value, s...
class DataHandler (line 335) | class DataHandler:
class Return (line 341) | class Return:
method __init__ (line 342) | def __init__(self, exp_code, data=None):
method ok (line 347) | def ok(self):
method __init__ (line 350) | def __init__(self, data_bank=None):
method __repr__ (line 364) | def __repr__(self):
method read_coils (line 367) | def read_coils(self, address, count, srv_info):
method write_coils (line 386) | def write_coils(self, address, bits_l, srv_info):
method read_d_inputs (line 405) | def read_d_inputs(self, address, count, srv_info):
method read_h_regs (line 424) | def read_h_regs(self, address, count, srv_info):
method write_h_regs (line 443) | def write_h_regs(self, address, words_l, srv_info):
method read_i_regs (line 462) | def read_i_regs(self, address, count, srv_info):
class DeviceIdentification (line 482) | class DeviceIdentification:
method __init__ (line 485) | def __init__(self, vendor_name=b'', product_code=b'', major_minor_revi...
method __getitem__ (line 523) | def __getitem__(self, key):
method __setitem__ (line 529) | def __setitem__(self, key, value):
method __repr__ (line 540) | def __repr__(self):
method vendor_name (line 569) | def vendor_name(self):
method vendor_name (line 573) | def vendor_name(self, value):
method product_code (line 577) | def product_code(self):
method product_code (line 581) | def product_code(self, value):
method major_minor_revision (line 585) | def major_minor_revision(self):
method major_minor_revision (line 589) | def major_minor_revision(self, value):
method vendor_url (line 593) | def vendor_url(self):
method vendor_url (line 597) | def vendor_url(self, value):
method product_name (line 601) | def product_name(self):
method product_name (line 605) | def product_name(self, value):
method model_name (line 609) | def model_name(self):
method model_name (line 613) | def model_name(self, value):
method user_application_name (line 617) | def user_application_name(self):
method user_application_name (line 621) | def user_application_name(self, value):
method items (line 624) | def items(self, start=0x00, end=0xff):
class ModbusServer (line 634) | class ModbusServer:
class Error (line 637) | class Error(Exception):
class NetworkError (line 641) | class NetworkError(Error):
class DataFormatError (line 645) | class DataFormatError(Error):
class ClientInfo (line 649) | class ClientInfo:
method __init__ (line 652) | def __init__(self, address='', port=0):
method __repr__ (line 656) | def __repr__(self):
class ServerInfo (line 659) | class ServerInfo:
method __init__ (line 662) | def __init__(self):
class SessionData (line 666) | class SessionData:
method __init__ (line 669) | def __init__(self):
method srv_info (line 675) | def srv_info(self):
method new_request (line 681) | def new_request(self):
method set_response_mbap (line 685) | def set_response_mbap(self):
class Frame (line 690) | class Frame:
method __init__ (line 691) | def __init__(self):
method raw (line 697) | def raw(self):
class MBAP (line 701) | class MBAP:
method __init__ (line 704) | def __init__(self, transaction_id=0, protocol_id=0, length=0, unit_i...
method raw (line 712) | def raw(self):
method raw (line 721) | def raw(self, value):
class PDU (line 734) | class PDU:
method __init__ (line 737) | def __init__(self, raw=b''):
method __len__ (line 746) | def __len__(self):
method func_code (line 750) | def func_code(self):
method except_code (line 754) | def except_code(self):
method is_except (line 758) | def is_except(self):
method is_valid (line 762) | def is_valid(self):
method clear (line 766) | def clear(self):
method build_except (line 769) | def build_except(self, func_code, exp_status):
method add_pack (line 774) | def add_pack(self, fmt, *args):
method unpack (line 781) | def unpack(self, fmt, from_byte=None, to_byte=None):
class ModbusService (line 789) | class ModbusService(BaseRequestHandler):
method server_running (line 792) | def server_running(self):
method _send_all (line 795) | def _send_all(self, data):
method _recv_all (line 802) | def _recv_all(self, size):
method setup (line 821) | def setup(self):
method handle (line 826) | def handle(self):
method __init__ (line 855) | def __init__(self, host='localhost', port=502, no_block=False, ipv6=Fa...
method __repr__ (line 921) | def __repr__(self):
method _engine (line 926) | def _engine(self, session_data):
method _internal_engine (line 940) | def _internal_engine(self, session_data):
method _read_bits (line 956) | def _read_bits(self, session_data):
method _read_words (line 992) | def _read_words(self, session_data):
method _write_single_coil (line 1022) | def _write_single_coil(self, session_data):
method _write_single_register (line 1044) | def _write_single_register(self, session_data):
method _write_multiple_coils (line 1064) | def _write_multiple_coils(self, session_data):
method _write_multiple_registers (line 1098) | def _write_multiple_registers(self, session_data):
method _write_read_multiple_registers (line 1132) | def _write_read_multiple_registers(self, session_data):
method _encapsulated_interface_transport (line 1178) | def _encapsulated_interface_transport(self, session_data):
method start (line 1250) | def start(self):
method stop (line 1284) | def stop(self):
method is_run (line 1291) | def is_run(self):
method _serve (line 1297) | def _serve(self):
FILE: pyModbusTCP/utils.py
function get_bits_from_int (line 11) | def get_bits_from_int(val_int, val_size=16):
function byte_length (line 35) | def byte_length(bit_length):
function test_bit (line 46) | def test_bit(value, offset):
function set_bit (line 60) | def set_bit(value, offset):
function reset_bit (line 74) | def reset_bit(value, offset):
function toggle_bit (line 88) | def toggle_bit(value, offset):
function word_list_to_long (line 105) | def word_list_to_long(val_list, big_endian=True, long_long=False):
function long_list_to_word (line 146) | def long_list_to_word(val_list, big_endian=True, long_long=False):
function get_2comp (line 183) | def get_2comp(val_int, val_size=16):
function get_list_2comp (line 212) | def get_list_2comp(val_list, val_size=16):
function decode_ieee (line 232) | def decode_ieee(val_int, double=False):
function encode_ieee (line 251) | def encode_ieee(val_float, double=False):
function crc16 (line 273) | def crc16(frame):
function valid_host (line 293) | def valid_host(host_str):
FILE: tests/test_client.py
class TestModbusClient (line 7) | class TestModbusClient(unittest.TestCase):
method test_host (line 10) | def test_host(self):
method test_port (line 25) | def test_port(self):
method test_unit_id (line 38) | def test_unit_id(self):
method test_misc (line 51) | def test_misc(self):
FILE: tests/test_client_server.py
class TestClientServer (line 20) | class TestClientServer(unittest.TestCase):
method setUp (line 23) | def setUp(self):
method tearDown (line 32) | def tearDown(self):
method test_default_startup_values (line 37) | def test_default_startup_values(self):
method test_read_write_requests (line 45) | def test_read_write_requests(self):
method test_server_strength (line 131) | def test_server_strength(self):
method test_server_read_identification (line 145) | def test_server_read_identification(self):
method test_client_read_identification (line 170) | def test_client_read_identification(self):
FILE: tests/test_server.py
class TestModbusServer (line 7) | class TestModbusServer(unittest.TestCase):
method test_device_identification (line 10) | def test_device_identification(self):
FILE: tests/test_utils.py
class TestUtils (line 11) | class TestUtils(unittest.TestCase):
method test_get_bits_from_int (line 14) | def test_get_bits_from_int(self):
method test_ieee (line 28) | def test_ieee(self):
method test_word_list_to_long (line 54) | def test_word_list_to_long(self):
method test_long_list_to_word (line 76) | def test_long_list_to_word(self):
method test_get_2comp (line 105) | def test_get_2comp(self):
method test_get_list_2comp (line 131) | def test_get_list_2comp(self):
Condensed preview — 57 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (207K chars).
[
{
"path": ".github/workflows/tests.yml",
"chars": 1355,
"preview": "# This workflow will install Python dependencies, run tests and lint with a variety of Python versions\n# For more inform"
},
{
"path": ".gitignore",
"chars": 75,
"preview": "/MANIFEST\n/build/\n/dist/\n/lab/\n*.pyc\n/*.egg-info/\n/.idea/\n/.vscode/\n/venv/\n"
},
{
"path": ".readthedocs.yaml",
"chars": 673,
"preview": "# .readthedocs.yaml\n# Read the Docs configuration file\n# See https://docs.readthedocs.io/en/stable/config-file/v2.html f"
},
{
"path": "CHANGES",
"chars": 6451,
"preview": "Revision history for pyModbusTCP\n\n0.3.1.dev0 xxxx-xx-xx\n\n - fix ModbusServer: debug messages now include OSError exce"
},
{
"path": "HOWTO-PyPi.md",
"chars": 1028,
"preview": "## How to upload on PyPI\n\nHere we use the twine tool to do the job, see [Twine setup](#twine-setup) to add and configure"
},
{
"path": "HOWTO-pkg-devel.md",
"chars": 481,
"preview": "## How to set package developer mode (also call editable mode on pip)\n\n*After set this, we can directly test effect of e"
},
{
"path": "LICENSE",
"chars": 1077,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2014 l.lefebvre\n\nPermission is hereby granted, free of charge, to any person obtain"
},
{
"path": "MANIFEST.in",
"chars": 104,
"preview": "include README.rst\ninclude setup.py\ninclude CHANGES\ninclude LICENSE\n\nrecursive-include pyModbusTCP *.py\n"
},
{
"path": "README.rst",
"chars": 3250,
"preview": ".. |badge_tests| image:: https://github.com/sourceperl/pyModbusTCP/actions/workflows/tests.yml/badge.svg?branch=master\n "
},
{
"path": "docs/conf.py",
"chars": 10006,
"preview": "# -*- coding: utf-8 -*-\n#\n# pyModbusTCP documentation build configuration file, created by\n# sphinx-quickstart on Thu Se"
},
{
"path": "docs/examples/client_float.rst",
"chars": 149,
"preview": "===============================\nClient: add float (inheritance)\n===============================\n\n\n.. literalinclude:: .."
},
{
"path": "docs/examples/client_minimal.rst",
"chars": 118,
"preview": "====================\nClient: minimal code\n====================\n\n\n.. literalinclude:: ../../examples/client_minimal.py\n"
},
{
"path": "docs/examples/client_read_coils.rst",
"chars": 115,
"preview": "==================\nClient: read coils\n==================\n\n\n.. literalinclude:: ../../examples/client_read_coils.py\n"
},
{
"path": "docs/examples/client_read_h_registers.rst",
"chars": 157,
"preview": "==============================\nClient: read holding registers\n==============================\n\n\n.. literalinclude:: ../.."
},
{
"path": "docs/examples/client_thread.rst",
"chars": 123,
"preview": "======================\nClient: polling thread\n======================\n\n\n.. literalinclude:: ../../examples/client_thread."
},
{
"path": "docs/examples/client_write_coils.rst",
"chars": 119,
"preview": "===================\nClient: write coils\n===================\n\n\n.. literalinclude:: ../../examples/client_write_coils.py\n"
},
{
"path": "docs/examples/index.rst",
"chars": 362,
"preview": "pyModbusTCP examples\n====================\n\n*Here some examples to see pyModbusTCP in some use cases*\n\n.. toctree::\n :m"
},
{
"path": "docs/examples/server.rst",
"chars": 107,
"preview": "===================\nServer: basic usage\n===================\n\n\n.. literalinclude:: ../../examples/server.py\n"
},
{
"path": "docs/examples/server_allow.rst",
"chars": 134,
"preview": "==========================\nServer: with an allow list\n==========================\n\n\n.. literalinclude:: ../../examples/se"
},
{
"path": "docs/examples/server_change_log.rst",
"chars": 139,
"preview": "==========================\nServer: with change logger\n==========================\n\n\n.. literalinclude:: ../../examples/se"
},
{
"path": "docs/examples/server_schedule.rst",
"chars": 152,
"preview": "===============================\nServer: schedule and alive word\n===============================\n\n\n.. literalinclude:: .."
},
{
"path": "docs/examples/server_serial_gw.rst",
"chars": 159,
"preview": "=================================\nServer: Modbus/TCP serial gateway\n=================================\n\n\n.. literalinclud"
},
{
"path": "docs/examples/server_virtual_data.rst",
"chars": 123,
"preview": "====================\nServer: virtual data\n====================\n\n\n.. literalinclude:: ../../examples/server_virtual_data."
},
{
"path": "docs/index.rst",
"chars": 177,
"preview": "Welcome to pyModbusTCP's documentation\n======================================\n\n.. toctree::\n :maxdepth: 2\n\n quicksta"
},
{
"path": "docs/package/class_ModbusClient.rst",
"chars": 436,
"preview": "Module pyModbusTCP.client\n=========================\n\n.. automodule:: pyModbusTCP.client\n\n*This module provide the Modbus"
},
{
"path": "docs/package/class_ModbusServer.rst",
"chars": 651,
"preview": "Module pyModbusTCP.server\n=========================\n\n.. automodule:: pyModbusTCP.server\n\n*This module provide the class "
},
{
"path": "docs/package/index.rst",
"chars": 171,
"preview": "pyModbusTCP modules documentation\n=================================\n\nContents:\n\n.. toctree::\n :maxdepth: 2\n\n class_M"
},
{
"path": "docs/package/module_utils.rst",
"chars": 744,
"preview": "Module pyModbusTCP.utils\n========================\n\n*This module provide a set of functions for modbus data mangling.*\n\nB"
},
{
"path": "docs/quickstart/index.rst",
"chars": 10964,
"preview": "Quick start guide\n=================\n\nOverview of the package\n-----------------------\n\npyModbusTCP give access to modbus/"
},
{
"path": "docs/quickstart/map.dot",
"chars": 449,
"preview": "/* \ngenerate pyModbusTCP map as PNG\n(need sudo apt-get install graphviz)\n\ncommand:\ndot -Tpng map.dot > map.png\n*/\n\ndigra"
},
{
"path": "docs/requirements.txt",
"chars": 24,
"preview": "sphinx-rtd-theme==1.3.0\n"
},
{
"path": "examples/README.md",
"chars": 258,
"preview": "## Important notice\n\n**The examples in this directory are designed to work with the version of pyModbusTCP currently in "
},
{
"path": "examples/client_debug.py",
"chars": 1055,
"preview": "#!/usr/bin/env python3\n\n\"\"\" An example of basic logging for ModbusClient debugging purposes. \"\"\"\n\nimport logging\nimport "
},
{
"path": "examples/client_float.py",
"chars": 1162,
"preview": "#!/usr/bin/env python3\n\n\"\"\" How-to add float support to ModbusClient. \"\"\"\n\nfrom pyModbusTCP.client import ModbusClient\nf"
},
{
"path": "examples/client_minimal.py",
"chars": 193,
"preview": "#!/usr/bin/env python3\n\n\"\"\" Minimal code example. \"\"\"\n\nfrom pyModbusTCP.client import ModbusClient\n\n# read 3 coils at @0"
},
{
"path": "examples/client_read_coils.py",
"chars": 550,
"preview": "#!/usr/bin/env python3\n\n\"\"\" Read 10 coils and print result on stdout. \"\"\"\n\nimport time\n\nfrom pyModbusTCP.client import M"
},
{
"path": "examples/client_read_h_registers.py",
"chars": 540,
"preview": "#!/usr/bin/env python3\n\n\"\"\" Read 10 holding registers and print result on stdout. \"\"\"\n\nimport time\n\nfrom pyModbusTCP.cli"
},
{
"path": "examples/client_serial_gw.py",
"chars": 8612,
"preview": "#!/usr/bin/env python3\n\n\"\"\"\nModbus RTU to TCP basic gateway (master attached)\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
},
{
"path": "examples/client_thread.py",
"chars": 1194,
"preview": "#!/usr/bin/env python3\n\n\"\"\"\nmodbus polling thread\n~~~~~~~~~~~~~~~~~~~~~\n\nStart a thread for polling a set of registers, "
},
{
"path": "examples/client_write_coils.py",
"chars": 952,
"preview": "#!/usr/bin/env python3\n\n\"\"\"Write 4 coils to True, wait 2s, write False and redo it.\"\"\"\n\nimport time\n\nfrom pyModbusTCP.cl"
},
{
"path": "examples/server.py",
"chars": 895,
"preview": "#!/usr/bin/env python3\n\n\"\"\"\nModbus/TCP server\n~~~~~~~~~~~~~~~~~\n\nRun this as root to listen on TCP privileged ports (<= "
},
{
"path": "examples/server_allow.py",
"chars": 2448,
"preview": "#!/usr/bin/env python3\n\n\"\"\"\nAn example of Modbus/TCP server which allow modbus read and/or write only from\nspecific IPs."
},
{
"path": "examples/server_change_log.py",
"chars": 1587,
"preview": "#!/usr/bin/env python3\n\n\"\"\"\nAn example of Modbus/TCP server with a change logger.\n\nRun this as root to listen on TCP pri"
},
{
"path": "examples/server_schedule.py",
"chars": 1453,
"preview": "#!/usr/bin/env python3\n\n\"\"\"\nModbus/TCP server with start/stop schedule\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nRun t"
},
{
"path": "examples/server_serial_gw.py",
"chars": 6883,
"preview": "#!/usr/bin/env python3\n\n\"\"\"\nModbus/TCP basic gateway (RTU slave(s) attached)\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
},
{
"path": "examples/server_virtual_data.py",
"chars": 1871,
"preview": "#!/usr/bin/env python3\n\n\"\"\"\nModbus/TCP server with virtual data\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nMap the system date"
},
{
"path": "pyModbusTCP/__init__.py",
"chars": 621,
"preview": "# Python package: Client and Server for ModBus/TCP\n# Website: https://github.com/sourceperl/pyModbusTCP\n# "
},
{
"path": "pyModbusTCP/client.py",
"chars": 36951,
"preview": "\"\"\" pyModbusTCP Client \"\"\"\n\nimport logging\nimport random\nimport socket\nimport struct\nfrom binascii import hexlify\nfrom d"
},
{
"path": "pyModbusTCP/constants.py",
"chars": 4012,
"preview": "\"\"\" pyModbusTCP package constants definition \"\"\"\n\n# Package version\nVERSION = '0.3.1.dev0'\n# Modbus/TCP\nMODBUS_PORT = 50"
},
{
"path": "pyModbusTCP/server.py",
"chars": 52959,
"preview": "\"\"\" pyModbusTCP Server \"\"\"\n\nimport logging\nimport socket\nimport struct\nfrom socketserver import BaseRequestHandler, Thre"
},
{
"path": "pyModbusTCP/utils.py",
"chars": 9219,
"preview": "\"\"\" pyModbusTCP utils functions \"\"\"\n\nimport re\nimport socket\nimport struct\n\n\n###############\n# bits function\n###########"
},
{
"path": "setup.cfg",
"chars": 48,
"preview": "[build_sphinx]\nsource-dir = docs/\nall_files = 1"
},
{
"path": "setup.py",
"chars": 459,
"preview": "from pyModbusTCP import constants\nfrom setuptools import setup\n\nwith open('README.rst') as f:\n readme = f.read()\n\nset"
},
{
"path": "tests/test_client.py",
"chars": 2237,
"preview": "\"\"\" Test of pyModbusTCP.ModbusClient \"\"\"\n\nimport unittest\nfrom pyModbusTCP.client import ModbusClient\n\n\nclass TestModbus"
},
{
"path": "tests/test_client_server.py",
"chars": 12391,
"preview": "\"\"\" Test of pyModbusTCP client-server interaction \"\"\"\n\nimport unittest\nfrom random import randint, getrandbits, choice\nf"
},
{
"path": "tests/test_server.py",
"chars": 1746,
"preview": "\"\"\" Test of pyModbusTCP.ModbusServer \"\"\"\n\nimport unittest\nfrom pyModbusTCP.server import ModbusServer, DeviceIdentificat"
},
{
"path": "tests/test_utils.py",
"chars": 6961,
"preview": "\"\"\" Test of pyModbusTCP.utils \"\"\"\n\nimport unittest\nimport math\nfrom pyModbusTCP.utils import \\\n get_bits_from_int, in"
}
]
About this extraction
This page contains the full source code of the sourceperl/pyModbusTCP GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 57 files (192.7 KB), approximately 49.0k tokens, and a symbol index with 250 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.