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 <> ~/.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 `_ 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. # " v 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 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(' 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(' 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}(? 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(encode_ieee(avogad, double=True), avo_64) self.assertAlmostEqual(encode_ieee(planck), pla_32) self.assertAlmostEqual(encode_ieee(planck, double=True), pla_64) def test_word_list_to_long(self): """Test function word_list_to_long and it 's short alias words2longs.""" # empty list, return empty list self.assertEqual(word_list_to_long([]), []) # if len of list is odd ignore last value self.assertEqual(word_list_to_long([0x1, 0x2, 0x3]), [0x10002]) # test convert with big and little endian l1 = [0xdead, 0xbeef] l2 = [0xfeed, 0xface, 0xcafe, 0xbeef] big = dict(big_endian=True) nobig = dict(big_endian=False) big64 = dict(big_endian=True, long_long=True) nobig64 = dict(big_endian=False, long_long=True) self.assertEqual(words2longs(l1, **big), [0xdeadbeef]) self.assertEqual(words2longs(l2, **big), [0xfeedface, 0xcafebeef]) self.assertEqual(words2longs(l1, **nobig), [0xbeefdead]) self.assertEqual(words2longs(l2, **nobig), [0xfacefeed, 0xbeefcafe]) self.assertEqual(words2longs(l1*2, **big64), [0xdeadbeefdeadbeef]) self.assertEqual(words2longs(l2*2, **big64), [0xfeedfacecafebeef]*2) self.assertEqual(words2longs(l1*2, **nobig64), [0xbeefdeadbeefdead]) self.assertEqual(words2longs(l2*2, **nobig64), [0xbeefcafefacefeed]*2) def test_long_list_to_word(self): """Test function long_list_to_word and short alias longs2words.""" # empty list, return empty list self.assertEqual(long_list_to_word([]), []) # test convert with big and little endian l1 = [0xdeadbeef] l1_big = [0xdead, 0xbeef] l1_nobig = [0xbeef, 0xdead] l1_big64 = [0x0000, 0x0000, 0xdead, 0xbeef] l1_nobig64 = [0xbeef, 0xdead, 0x0000, 0x0000] l2 = [0xfeedface, 0xcafebeef] l2_big = [0xfeed, 0xface, 0xcafe, 0xbeef] l2_nobig = [0xface, 0xfeed, 0xbeef, 0xcafe] l3 = [0xfeedfacecafebeef] l3_big64 = [0xfeed, 0xface, 0xcafe, 0xbeef] l3_nobig64 = [0xbeef, 0xcafe, 0xface, 0xfeed] big = dict(big_endian=True) nobig = dict(big_endian=False) big64 = dict(big_endian=True, long_long=True) nobig64 = dict(big_endian=False, long_long=True) self.assertEqual(longs2words(l1, **big), l1_big) self.assertEqual(longs2words(l2, **big), l2_big) self.assertEqual(longs2words(l1, **nobig), l1_nobig) self.assertEqual(longs2words(l2, **nobig), l2_nobig) self.assertEqual(longs2words(l1*2, **big64), l1_big64*2) self.assertEqual(longs2words(l3*2, **big64), l3_big64*2) self.assertEqual(longs2words(l1*4, **nobig64), l1_nobig64*4) self.assertEqual(longs2words(l3*4, **nobig64), l3_nobig64*4) def test_get_2comp(self): """Test function get_2comp and it's short alias twos_c.""" # check if ValueError exception is raised self.assertRaises(ValueError, get_2comp, 0x10000) self.assertRaises(ValueError, get_2comp, -0x8001) self.assertRaises(ValueError, twos_c, 0x100000000, val_size=32) self.assertRaises(ValueError, twos_c, -0x80000001, val_size=32) # 2's complement of 16bits values (default) self.assertEqual(get_2comp(0x0001), 0x0001) self.assertEqual(get_2comp(0x8000), -0x8000) self.assertEqual(get_2comp(-0x8000), 0x8000) self.assertEqual(get_2comp(0xffff), -0x0001) self.assertEqual(get_2comp(-0x0001), 0xffff) self.assertEqual(get_2comp(-0x00fa), 0xff06) self.assertEqual(get_2comp(0xff06), -0x00fa) # 2's complement of 32bits values self.assertEqual(twos_c(0xfffffff, val_size=32), 0xfffffff) self.assertEqual(twos_c(-1, val_size=32), 0xffffffff) self.assertEqual(twos_c(0xffffffff, val_size=32), -1) self.assertEqual(twos_c(125, val_size=32), 0x0000007d) self.assertEqual(twos_c(0x0000007d, val_size=32), 125) self.assertEqual(twos_c(-250, val_size=32), 0xffffff06) self.assertEqual(twos_c(0xffffff06, val_size=32), -250) self.assertEqual(twos_c(0xfffea2a5, val_size=32), -89435) self.assertEqual(twos_c(-89435, val_size=32), 0xfffea2a5) def test_get_list_2comp(self): """Test get_list_2comp and it's short alias twos_c_l.""" self.assertEqual(get_list_2comp([0x8000], 16), [-32768]) in_l = [0x8000, 0xffff, 0x0042] out_l = [-0x8000, -0x0001, 0x42] self.assertEqual(twos_c_l(in_l, val_size=16), out_l) in_l = [0x8000, 0xffffffff, 0xfffea2a5] out_l = [0x8000, -0x0001, -89435] self.assertEqual(twos_c_l(in_l, val_size=32), out_l) if __name__ == '__main__': unittest.main()