pax_global_header00006660000000000000000000000064137220124710014511gustar00rootroot0000000000000052 comment=4fd98bab9fe320260b9bc0b0eb5357f043aa6ec3 AdvancedClimateSystems-uModbus-4fd98ba/000077500000000000000000000000001372201247100202245ustar00rootroot00000000000000AdvancedClimateSystems-uModbus-4fd98ba/.gitignore000066400000000000000000000014351372201247100222170ustar00rootroot00000000000000# Created by https://www.gitignore.io/api/python ### Python ### # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ .env .ropeproject AdvancedClimateSystems-uModbus-4fd98ba/.travis.yml000066400000000000000000000004101372201247100223300ustar00rootroot00000000000000language: python python: - 3.8 - 3.7 - 3.6 - 3.5 - 3.4 - 2.7 install: - pip install -r dev_requirements.txt - pip install coveralls script: py.test --cov-report term-missing --cov=umodbus -v tests/ after_success: coveralls AdvancedClimateSystems-uModbus-4fd98ba/LICENSE000066400000000000000000000370611372201247100212400ustar00rootroot00000000000000Mozilla Public License, version 2.0 1. Definitions 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means a. that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or b. that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: a. any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or b. any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: a. under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and b. under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: a. for any code that a Contributor has removed from Covered Software; or b. for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or c. under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: a. such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and b. You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. 6. Disclaimer of Warranty Covered Software is provided under this License on an "as is" basis, without warranty of any kind, either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software is free of defects, merchantable, fit for a particular purpose or non-infringing. The entire risk as to the quality and performance of the Covered Software is with You. Should any Covered Software prove defective in any respect, You (not any Contributor) assume the cost of any necessary servicing, repair, or correction. This disclaimer of warranty constitutes an essential part of this License. No use of any Covered Software is authorized under this License except under this disclaimer. 7. Limitation of Liability Under no circumstances and under no legal theory, whether tort (including negligence), contract, or otherwise, shall any Contributor, or anyone who distributes Covered Software as permitted above, be liable to You for any direct, indirect, special, incidental, or consequential damages of any character including, without limitation, damages for lost profits, loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses, even if such party shall have been informed of the possibility of such damages. This limitation of liability shall not apply to liability for death or personal injury resulting from such party's negligence to the extent applicable law prohibits such limitation. Some jurisdictions do not allow the exclusion or limitation of incidental or consequential damages, so this exclusion and limitation may not apply to You. 8. Litigation Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. AdvancedClimateSystems-uModbus-4fd98ba/README.rst000066400000000000000000000103721372201247100217160ustar00rootroot00000000000000.. image:: https://travis-ci.org/AdvancedClimateSystems/uModbus.svg :target: https://travis-ci.org/AdvancedClimateSystems/uModbus .. image:: https://coveralls.io/repos/AdvancedClimateSystems/uModbus/badge.svg?service=github :target: https://coveralls.io/github/AdvancedClimateSystems/uModbus .. image:: https://img.shields.io/pypi/v/uModbus.svg :target: https://pypi.python.org/pypi/uModbus .. image:: https://img.shields.io/pypi/pyversions/uModbus.svg :target: https://pypi.python.org/pypi/uModbus uModbus ======= uModbus or (μModbus) is a pure Python implementation of the Modbus protocol as described in the `MODBUS Application Protocol Specification V1.1b3`_. uModbus implements both a Modbus client (both TCP and RTU) and a Modbus server (both TCP and RTU). The "u" or "μ" in the name comes from the the SI prefix "micro-". uModbus is very small and lightweight. The source can be found on GitHub_. Documentation is available at `Read the Docs`_. Quickstart ---------- Creating a Modbus TCP server is easy: .. Because GitHub doesn't support the include directive the source of scripts/examples/simple_tcp_server.py has been copied to this file. .. code:: python #!/usr/bin/env python # scripts/examples/simple_tcp_server.py import logging from socketserver import TCPServer from collections import defaultdict from umodbus import conf from umodbus.server.tcp import RequestHandler, get_server from umodbus.utils import log_to_stream # Add stream handler to logger 'uModbus'. log_to_stream(level=logging.DEBUG) # A very simple data store which maps addresss against their values. data_store = defaultdict(int) # Enable values to be signed (default is False). conf.SIGNED_VALUES = True TCPServer.allow_reuse_address = True app = get_server(TCPServer, ('localhost', 502), RequestHandler) @app.route(slave_ids=[1], function_codes=[3, 4], addresses=list(range(0, 10))) def read_data_store(slave_id, function_code, address): """" Return value of address. """ return data_store[address] @app.route(slave_ids=[1], function_codes=[6, 16], addresses=list(range(0, 10))) def write_data_store(slave_id, function_code, address, value): """" Set value for address. """ data_store[address] = value if __name__ == '__main__': try: app.serve_forever() finally: app.shutdown() app.server_close() Doing a Modbus request requires even less code: .. Because GitHub doesn't support the include directive the source of scripts/examples/simple_data_store.py has been copied to this file. .. code:: python #!/usr/bin/env python # scripts/examples/simple_tcp_client.py import socket from umodbus import conf from umodbus.client import tcp # Enable values to be signed (default is False). conf.SIGNED_VALUES = True sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(('localhost', 502)) # Returns a message or Application Data Unit (ADU) specific for doing # Modbus TCP/IP. message = tcp.write_multiple_coils(slave_id=1, starting_address=1, values=[1, 0, 1, 1]) # Response depends on Modbus function code. This particular returns the # amount of coils written, in this case it is. response = tcp.send_message(message, sock) sock.close() Features -------- The following functions have been implemented for Modbus TCP and Modbus RTU: * 01: Read Coils * 02: Read Discrete Inputs * 03: Read Holding Registers * 04: Read Input Registers * 05: Write Single Coil * 06: Write Single Register * 15: Write Multiple Coils * 16: Write Multiple Registers Other featues: * Support for signed and unsigned register values. License ------- uModbus software is licensed under `Mozilla Public License`_. © 2018 `Advanced Climate Systems`_. .. External References: .. _Advanced Climate Systems: http://www.advancedclimate.nl/ .. _GitHub: https://github.com/AdvancedClimateSystems/uModbus/ .. _MODBUS Application Protocol Specification V1.1b3: http://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf .. _Mozilla Public License: https://github.com/AdvancedClimateSystems/uModbus/blob/develop/LICENSE .. _Read the Docs: http://umodbus.readthedocs.org/en/latest/ AdvancedClimateSystems-uModbus-4fd98ba/dev_requirements.txt000066400000000000000000000003561372201247100243520ustar00rootroot00000000000000-r requirements.txt mock==3.0.5;python_version<"3.3" pytest==5.3.1;python_version>="3.5" pytest==4.6.6;python_version<"3.5" pytest-cov==2.8.1 Sphinx==2.2.2;python_version>="3.5" Sphinx==1.8.5;python_version<"3.5" sphinx-rtd-theme==0.4.3 AdvancedClimateSystems-uModbus-4fd98ba/docs/000077500000000000000000000000001372201247100211545ustar00rootroot00000000000000AdvancedClimateSystems-uModbus-4fd98ba/docs/Makefile000066400000000000000000000163721372201247100226250ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Modbus.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Modbus.qhc" applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Modbus" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Modbus" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." AdvancedClimateSystems-uModbus-4fd98ba/docs/source/000077500000000000000000000000001372201247100224545ustar00rootroot00000000000000AdvancedClimateSystems-uModbus-4fd98ba/docs/source/changelog.rst000066400000000000000000000133411372201247100251370ustar00rootroot00000000000000Changelog ========= 1.0.4 (2020-08-27) ++++++++++++++++++ **Bugs** * `#90`_ Fix error code of 2 Modbus errors. Thanks `@rgov`! * `#100`_ Improve check for missing routes. Thanks `@rgov`! * `#101`_ Fix crash if 1 of arguments of `umodbus.server.route` is `None` .Thanks `@rgov`! * `#105`_ Fix byte count when for WriteMultipleCoils. Thank `@acolomb`! **Improvements** * `#102`_ Remove redundant exception traceback. Thanks `@rgov`! * `#103`_ Fix error code of 2 Modbus errors. Thanks `@rgov`! * `#104`_ Denote hex dump of ADU in debug log. Thanks `@rgov`! .. _#90: https://github.com/AdvancedClimateSystems/uModbus/issues/90 .. _#100: https://github.com/AdvancedClimateSystems/uModbus/issues/100 .. _#101: https://github.com/AdvancedClimateSystems/uModbus/issues/101 .. _#102: https://github.com/AdvancedClimateSystems/uModbus/issues/102 .. _#103: https://github.com/AdvancedClimateSystems/uModbus/issues/103 .. _#104: https://github.com/AdvancedClimateSystems/uModbus/issues/103 .. _#105: https://github.com/AdvancedClimateSystems/uModbus/issues/105 1.0.3 (2019-12-04) ++++++++++++++++++ * `#76`_ Remove use of deprecated `inspect.getargspec()` for Python>=3.5. * Drop support for Python 3.3 * Add support for Python 3.7 and Python 3.8 .. _#76: https://github.com/AdvancedClimateSystems/uModbus/issues/76 1.0.2 (2018-05-22) ++++++++++++++++++ I released uModbus 1.0.1 without updating the version number in `setup.py`. This releases fixes this. 1.0.1 (2018-05-22) ++++++++++++++++++ `@wthomson`_ has fixed a couple of typo's in the documentation. Thanks! **Bugs** * `#49`_ Fix clients being to greedy when reading response. Thanks `@lutostag`_! .. _#49: https://github.com/AdvancedClimateSystems/uModbus/issues/49 .. _@lutostag: https://github.com/lutostag .. _@wthomson: https://github.com/wthomson 1.0.0 (2018-01-06) ++++++++++++++++++ **Bugs** * `#50`_ Fix handling of empty ADU's. .. _#50: https://github.com/AdvancedClimateSystems/uModbus/issues/50 0.8.2 (2016-11-11) ++++++++++++++++++ **Bugs** * `#47`_ Fix import errors in sample code. Thanks `@tiagocoutinho`_! .. _#47: https://github.com/AdvancedClimateSystems/uModbus/issues/47 .. _@tiagocoutinho: https://github.com/tiagocoutinho 0.8.1 (2016-11-02) ++++++++++++++++++ **Bugs** * `#27`_ Route is called with wrong value when one write single coil with value 1. * `#42`_ Drop support for PyPy. .. _#27: https://github.com/AdvancedClimateSystems/uModbus/issues/27 .. _#42: https://github.com/AdvancedClimateSystems/uModbus/issues/42 0.8.0 (2016-10-31) ++++++++++++++++++ **Features** * `#48`_ Update to pyserial 3.2.1 .. _#48: https://github.com/AdvancedClimateSystems/uModbus/issues/48 0.7.2 (2016-09-27) ++++++++++++++++++ **Bugs** * `#44`_ Remove print statement. * `#46`_ Transaction ID overflow. Thanks `@greg0pearce`_ .. _#44: https://github.com/AdvancedClimateSystems/uModbus/issues/44 .. _#46: https://github.com/AdvancedClimateSystems/uModbus/issues/46 .. _@greg0pearce`: https://github.com/greg0pearce 0.7.1 (01-09-2016) ++++++++++++++++++ **Bugs** * `#41`_ RTU server doesn't handle frames correct. .. _#41: https://github.com/AdvancedClimateSystems/uModbus/issues/41 0.7.0 (29-07-2016) ++++++++++++++++++ **Features** * `#22`_ Add Modbus RTU server. **Bugs** * `#39`_ Merge functions module with _functions package. * `#37`_ Pretty print binary data in shell. * `#38`_ Fix type in sumple_rtu_client.py .. _#22: https://github.com/AdvancedClimateSystems/uModbus/issues/22 .. _#29: https://github.com/AdvancedClimateSystems/uModbus/issues/29 .. _#37: https://github.com/AdvancedClimateSystems/uModbus/issues/37 .. _#38: https://github.com/AdvancedClimateSystems/uModbus/issues/38 0.6.0 (2016-05-08) ++++++++++++++++++ **Features** * `#24`_ Add Modbus RTU client. .. _#24: https://github.com/AdvancedClimateSystems/uModbus/issues/24 0.5.0 (2016-05-03) ++++++++++++++++++ **Bugs** * `#36`_ Parameter `function_code` is missing in signature of routes. .. _#36: https://github.com/AdvancedClimateSystems/uModbus/issues/36 0.4.2 (2016-04-07) ++++++++++++++++++ **Bugs** * `#20`_ uModbus should close connection when client closes it. .. _#20: https://github.com/AdvancedClimateSystems/uModbus/issues/20 0.4.1 (2016-01-22) ++++++++++++++++++ **Bugs** * `#31`_ Add subpackages `umodbus.client` and `umodbus._functions` to `setup.py`. .. _#31: https://github.com/AdvancedClimateSystems/uModbus/issues/31 0.4.0 (2016-01-22) ++++++++++++++++++ **Features** * `#23`_ Implemenent Modbus client. * `#28`_ Implemenent signed integers for Modbus client. .. _#23: https://github.com/AdvancedClimateSystems/uModbus/issues/23 .. _#28: https://github.com/AdvancedClimateSystems/uModbus/issues/28 0.3.1 (2015-12-12) ++++++++++++++++++ **Bugs** * `#18`_ Edit interface of `get_server` so socket options can now be set easily. .. _#18: https://github.com/AdvancedClimateSystems/uModbus/issues/18 0.3.0 (2015-12-05) ++++++++++++++++++ **Features** * `#17`_ `RequestHandler.handle()` can be overridden easily. .. _#17: https://github.com/AdvancedClimateSystems/uModbus/issues/17 0.2.0 (2015-11-19) ++++++++++++++++++ **Features** * `#10`_ Support for signed values. **Bugs** * `#13`_ Fix shutdown of server in `simple_data_store.py` .. _#10: https://github.com/AdvancedClimateSystems/uModbus/issues/10 .. _#13: https://github.com/AdvancedClimateSystems/uModbus/issues/13 0.1.2 (2015-11-16) ++++++++++++++++++ **Bugs** * `#8`_ `WriteMultipleCoils.create_from_request_pdu` sometimes doesn't unpack PDU correct. .. _#8: https://github.com/AdvancedClimateSystems/uModbus/issues/8 0.1.1 (2015-11-12) ++++++++++++++++++ **Bugs** * `#7`_ Fix default stream and log level of `utils.log_to_stream`. .. _#7: https://github.com/AdvancedClimateSystems/uModbus/issues/7 0.1.0 (2015-11-10) ++++++++++++++++++ * First release. AdvancedClimateSystems-uModbus-4fd98ba/docs/source/client/000077500000000000000000000000001372201247100237325ustar00rootroot00000000000000AdvancedClimateSystems-uModbus-4fd98ba/docs/source/client/index.rst000066400000000000000000000002031372201247100255660ustar00rootroot00000000000000Modbus Client ============= uModbus contains a client for Modbus TCP and Modbus RTU. .. toctree:: :maxdepth: 2 tcp rtu AdvancedClimateSystems-uModbus-4fd98ba/docs/source/client/rtu.rst000066400000000000000000000021721372201247100253000ustar00rootroot00000000000000Modbus RTU ---------- Example ======= .. note:: uModbus doesn't support all the functions defined for Modbus RTU. It currently supports the following functions: * 01: Read Coils * 02: Read Discrete Inputs * 03: Read Holding Registers * 04: Read Input Registers * 05: Write Single Coil * 06: Write Single Register * 15: Write Multiple Coils * 16: Write Multiple Registers .. include:: ../../../scripts/examples/simple_rtu_client.py :code: python API === .. autofunction:: umodbus.client.serial.rtu.send_message .. autofunction:: umodbus.client.serial.rtu.parse_response_adu .. autofunction:: umodbus.client.serial.rtu.read_coils .. autofunction:: umodbus.client.serial.rtu.read_discrete_inputs .. autofunction:: umodbus.client.serial.rtu.read_holding_registers .. autofunction:: umodbus.client.serial.rtu.read_input_registers .. autofunction:: umodbus.client.serial.rtu.write_single_coil .. autofunction:: umodbus.client.serial.rtu.write_single_register .. autofunction:: umodbus.client.serial.rtu.write_multiple_coils .. autofunction:: umodbus.client.serial.rtu.write_multiple_registers AdvancedClimateSystems-uModbus-4fd98ba/docs/source/client/tcp.rst000066400000000000000000000014241372201247100252530ustar00rootroot00000000000000Modbus TCP ---------- Example ======= All function codes for Modbus TCP/IP are supported. You can use the client like this: .. include:: ../../../scripts/examples/simple_tcp_client.py :code: python API === .. autofunction:: umodbus.client.tcp.send_message .. autofunction:: umodbus.client.tcp.parse_response_adu .. autofunction:: umodbus.client.tcp.read_coils .. autofunction:: umodbus.client.tcp.read_discrete_inputs .. autofunction:: umodbus.client.tcp.read_holding_registers .. autofunction:: umodbus.client.tcp.read_input_registers .. autofunction:: umodbus.client.tcp.write_single_coil .. autofunction:: umodbus.client.tcp.write_single_register .. autofunction:: umodbus.client.tcp.write_multiple_coils .. autofunction:: umodbus.client.tcp.write_multiple_registers AdvancedClimateSystems-uModbus-4fd98ba/docs/source/conf.py000066400000000000000000000224301372201247100237540ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # Modbus documentation build configuration file, created by # sphinx-quickstart on Tue Oct 13 21:13:48 2015. # # 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. import sys import os import shlex # 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. sys.path.insert(0, os.path.abspath('../../')) # -- 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.ext.viewcode', ] # 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 = 'uModbus' copyright = '2019, Auke Willem Oosterhoff ' author = 'Auke Willem Oosterhoff ' # 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 = '1.0' # The full version, including alpha/beta/rc tags. release = '1.0.4' # 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 = None # 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. exclude_patterns = [] # 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 = 'alabaster' # 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 = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # 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 (within the static path) to use as 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 '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # 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 = { 'index': ['globaltoc.html', 'searchbox.html'], '**': ['globaltoc.html', 'relations.html', 'searchbox.html'] } # 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', 'h', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' #html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value #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 = 'Modbusdoc' # -- 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, 'Modbus.tex', 'Modbus Documentation', 'Auke Willem Oosterhoff \\textless{}a.oosterhoff@climotion.com\\textgreater{}', '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 = [] # 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, 'modbus', 'Modbus 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, 'Modbus', 'Modbus Documentation', author, 'Modbus', '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 autodoc_member_order = 'bysource' AdvancedClimateSystems-uModbus-4fd98ba/docs/source/configuration.rst000066400000000000000000000004761372201247100260640ustar00rootroot00000000000000Configuration ============= :attr:`umodbus.conf` is a global configuration object and is an instance of `umodbus.config.Config`. It can be used like this: .. code:: python from umodbus import conf conf.SIGNED_VALUES = True .. module:: umodbus.config .. autoclass:: Config :members: SIGNED_VALUES AdvancedClimateSystems-uModbus-4fd98ba/docs/source/decompose_requests.rst000066400000000000000000000020771372201247100271250ustar00rootroot00000000000000Decompose requests ------------------ Modbus requests and responses contain an Application Data Unit (ADU) which contains a Protocol Data Unit (PDU). The ADU is an envelope containing a message, the PDU is the message itself. Modbus requests can be sent via two communication layers, RTU or TCP/IP. The ADU for these layers differs. But the PDU, the message, always has the same strcuture, regardless of the way it's transported. PDU === .. automodule:: umodbus.functions ADU for TCP/IP requests and responses ===================================== .. automodule:: umodbus.client.tcp .. _MODBUS Messaging on TCP/IP Implementation Guide V1.0b: http://modbus.org/docs/Modbus_Messaging_Implementation_Guide_V1_0b.pdf .. _MODBUS Application Protocol Specification V1.1b3: http://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf ADU for RTU requests and responses ================================== .. automodule:: umodbus.client.serial.rtu .. _MODBUS over Serial Line Specification and Implementation Guide V1.02: http://www.modbus.org/docs/Modbus_over_serial_line_V1_02.pdf AdvancedClimateSystems-uModbus-4fd98ba/docs/source/functions.rst000066400000000000000000000016001372201247100252130ustar00rootroot00000000000000Modbus Functions ---------------- These Modbus functions have been implemented. 01: Read Coils ============== .. autoclass:: umodbus.functions.ReadCoils 02: Read Discrete Inputs ======================== .. autoclass:: umodbus.functions.ReadDiscreteInputs 03: Read Holding Registers ========================== .. autoclass:: umodbus.functions.ReadHoldingRegisters 04: Read Input Registers ======================== .. autoclass:: umodbus.functions.ReadInputRegisters 05: Write Single Coil ===================== .. autoclass:: umodbus.functions.WriteSingleCoil 06: Write Single Register ========================= .. autoclass:: umodbus.functions.WriteSingleRegister 15: Write Multiple Coils ======================== .. autoclass:: umodbus.functions.WriteMultipleCoils 16: Write Multiple Registers ============================ .. autoclass:: umodbus.functions.WriteMultipleRegisters AdvancedClimateSystems-uModbus-4fd98ba/docs/source/index.rst000066400000000000000000000004651372201247100243220ustar00rootroot00000000000000.. include:: ../../README.rst .... How uModus works ---------------- .. toctree:: :maxdepth: 2 installation modbus_server client/index configuration changelog The Modbus protocol explained ----------------------------- .. toctree:: :maxdepth: 2 decompose_requests functions AdvancedClimateSystems-uModbus-4fd98ba/docs/source/installation.rst000066400000000000000000000021001372201247100257000ustar00rootroot00000000000000Installation ------------ uModbus has been tested_ on Python 2.7 and Python 3.3+. As package ========== uModbus is available on Pypi_ and can be installed through Pip_:: $ pip install umodbus Or you can install from source_ using `setup.py`:: $ python setup.py install For development, debugging and testing ====================================== uModbus has no runtime dependencies. However to run the the tests or build the documentation some dependencies are required. They are listed in dev_requirements.txt_ and can be installed through Pip:: $ pip install -r dev_requirements.txt Now you can build the docs:: $ sphinx-build -b html docs/source docs/build Or run the tests:: $ py.test tests .. External references: .. _dev_requirements.txt: https://github.com/AdvancedClimateSystems/uModbus/blob/develop/dev_requirements.txt .. _Pypi: https://pypi.python.org/pypi/uModbus .. _Pip: https://pip.readthedocs.org/en/stable/ .. _source: https://github.com/AdvancedClimateSystems/umodbus .. _tested: https://travis-ci.org/AdvancedClimateSystems/uModbus AdvancedClimateSystems-uModbus-4fd98ba/docs/source/modbus_server.rst000066400000000000000000000020061372201247100260630ustar00rootroot00000000000000Modbus Server ------------- Viewpoint ========= The uModbus server code is built with routing in mind. Routing (groups of) requests to a certain callback is easy. This is in contrast with with other Modbus implementation which often focus on reading and writing from a data store. Because of this difference in viewpoint uModbus doesn't know the concept of Modbus' data models like discrete inputs, coils, input registers, holding registers and their read/write properties. Routing ======= The routing system was inspired by Flask_. Like Flask, uModbus requires a global app or server. This server contains a route map. Routes can be added to the route map. The following code example demonstrates how to implement a very simple data store for 10 addresses. Modbus TCP example ================== .. include:: ../../scripts/examples/simple_tcp_server.py :code: python Modbus RTU example ================== .. include:: ../../scripts/examples/simple_rtu_server.py :code: python .. _Flask: http://flask.pocoo.org/ AdvancedClimateSystems-uModbus-4fd98ba/requirements.txt000066400000000000000000000000161372201247100235050ustar00rootroot00000000000000pyserial==3.4 AdvancedClimateSystems-uModbus-4fd98ba/scripts/000077500000000000000000000000001372201247100217135ustar00rootroot00000000000000AdvancedClimateSystems-uModbus-4fd98ba/scripts/examples/000077500000000000000000000000001372201247100235315ustar00rootroot00000000000000AdvancedClimateSystems-uModbus-4fd98ba/scripts/examples/simple_rtu_client.py000077500000000000000000000017141372201247100276320ustar00rootroot00000000000000#!/usr/bin/env python # scripts/example/simple_rtu_client.py import fcntl import struct from serial import Serial, PARITY_NONE from umodbus.client.serial import rtu def get_serial_port(): """ Return serial.Serial instance, ready to use for RS485.""" port = Serial(port='/dev/ttyS1', baudrate=9600, parity=PARITY_NONE, stopbits=1, bytesize=8, timeout=1) fh = port.fileno() # A struct with configuration for serial port. serial_rs485 = struct.pack('hhhhhhhh', 1, 0, 0, 0, 0, 0, 0, 0) fcntl.ioctl(fh, 0x542F, serial_rs485) return port serial_port = get_serial_port() # Returns a message or Application Data Unit (ADU) specific for doing # Modbus RTU. message = rtu.write_multiple_coils(slave_id=1, address=1, values=[1, 0, 1, 1]) # Response depends on Modbus function code. This particular returns the # amount of coils written, in this case it is. response = rtu.send_message(message, serial_port) serial_port.close() AdvancedClimateSystems-uModbus-4fd98ba/scripts/examples/simple_rtu_server.py000077500000000000000000000014401372201247100276560ustar00rootroot00000000000000#!/usr/bin/env python from serial import Serial from collections import defaultdict from umodbus.server.serial import get_server from umodbus.server.serial.rtu import RTUServer s = Serial('/dev/ttyS1') s.timeout = 10 data_store = defaultdict(int) app = get_server(RTUServer, s) @app.route(slave_ids=[1], function_codes=[1, 2], addresses=list(range(0, 10))) def read_data_store(slave_id, function_code, address): """" Return value of address. """ return data_store[address] @app.route(slave_ids=[1], function_codes=[5, 15], addresses=list(range(0, 10))) def write_data_store(slave_id, function_code, address, value): """" Set value for address. """ data_store[address] = value if __name__ == '__main__': try: app.serve_forever() finally: app.shutdown() AdvancedClimateSystems-uModbus-4fd98ba/scripts/examples/simple_tcp_client.py000077500000000000000000000012151372201247100276020ustar00rootroot00000000000000#!/usr/bin/env python # scripts/examples/simple_tcp_client.py import socket from umodbus import conf from umodbus.client import tcp # Enable values to be signed (default is False). conf.SIGNED_VALUES = True sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(('localhost', 502)) # Returns a message or Application Data Unit (ADU) specific for doing # Modbus TCP/IP. message = tcp.write_multiple_coils(slave_id=1, starting_address=1, values=[1, 0, 1, 1]) # Response depends on Modbus function code. This particular returns the # amount of coils written, in this case it is. response = tcp.send_message(message, sock) sock.close() AdvancedClimateSystems-uModbus-4fd98ba/scripts/examples/simple_tcp_server.py000077500000000000000000000022271372201247100276360ustar00rootroot00000000000000#!/usr/bin/env python # scripts/examples/simple_data_store.py import logging from socketserver import TCPServer from collections import defaultdict from umodbus import conf from umodbus.server.tcp import RequestHandler, get_server from umodbus.utils import log_to_stream # Add stream handler to logger 'uModbus'. log_to_stream(level=logging.DEBUG) # A very simple data store which maps addresses against their values. data_store = defaultdict(int) # Enable values to be signed (default is False). conf.SIGNED_VALUES = True TCPServer.allow_reuse_address = True app = get_server(TCPServer, ('localhost', 502), RequestHandler) @app.route(slave_ids=[1], function_codes=[1, 2], addresses=list(range(0, 10))) def read_data_store(slave_id, function_code, address): """" Return value of address. """ return data_store[address] @app.route(slave_ids=[1], function_codes=[5, 15], addresses=list(range(0, 10))) def write_data_store(slave_id, function_code, address, value): """" Set value for address. """ data_store[address] = value if __name__ == '__main__': try: app.serve_forever() finally: app.shutdown() app.server_close() AdvancedClimateSystems-uModbus-4fd98ba/setup.cfg000066400000000000000000000003751372201247100220520ustar00rootroot00000000000000[bdist_wheel] # This flag says that the code is written to work on both Python 2 and Python # 3. If at all possible, it is good practice to do this. If you cannot, you # will need to generate wheels for each Python version that you support. universal=1 AdvancedClimateSystems-uModbus-4fd98ba/setup.py000077500000000000000000000027251372201247100217470ustar00rootroot00000000000000#!/usr/bin/env python """ uModbus is a pure Python implementation of the Modbus protocol with support for Python 2.7, 3.4, 3.5, 3.6, 3.7 and 3.8. """ import os from setuptools import setup cwd = os.path.dirname(os.path.abspath(__name__)) long_description = open(os.path.join(cwd, 'README.rst'), 'r').read() setup(name='uModbus', version='1.0.4', author='Auke Willem Oosterhoff', author_email='a.oosterhoff@climotion.com', description='Implementation of the Modbus protocol in pure Python.', url='https://github.com/AdvancedClimateSystems/umodbus/', long_description=long_description, license='MPL', packages=[ 'umodbus', 'umodbus.client', 'umodbus.client.serial', 'umodbus.server', 'umodbus.server.serial', ], install_requires=[ 'pyserial~=3.4', ], classifiers=[ 'Development Status :: 6 - Mature', 'Intended Audience :: Developers', 'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)', 'Operating System :: OS Independent', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Topic :: Software Development :: Embedded Systems', ]) AdvancedClimateSystems-uModbus-4fd98ba/tests/000077500000000000000000000000001372201247100213665ustar00rootroot00000000000000AdvancedClimateSystems-uModbus-4fd98ba/tests/__init__.py000066400000000000000000000000001372201247100234650ustar00rootroot00000000000000AdvancedClimateSystems-uModbus-4fd98ba/tests/system/000077500000000000000000000000001372201247100227125ustar00rootroot00000000000000AdvancedClimateSystems-uModbus-4fd98ba/tests/system/__init__.py000066400000000000000000000000001372201247100250110ustar00rootroot00000000000000AdvancedClimateSystems-uModbus-4fd98ba/tests/system/conftest.py000066400000000000000000000013141372201247100251100ustar00rootroot00000000000000import struct import pytest import socket from threading import Thread from .tcp_server import app as tcp from .rtu_server import app as rtu @pytest.fixture(autouse=True, scope="session") def tcp_server(request): t = Thread(target=tcp.serve_forever) t.start() def fin(): tcp.shutdown() tcp.server_close() t.join() request.addfinalizer(fin) return tcp @pytest.yield_fixture def sock(tcp_server): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(tcp_server.socket.getsockname()) yield sock sock.close() @pytest.fixture def rtu_server(): return rtu @pytest.fixture def mbap(): return struct.pack('>HHHB', 0, 0, 6, 1) AdvancedClimateSystems-uModbus-4fd98ba/tests/system/responses/000077500000000000000000000000001372201247100247335ustar00rootroot00000000000000AdvancedClimateSystems-uModbus-4fd98ba/tests/system/responses/__init__.py000066400000000000000000000000001372201247100270320ustar00rootroot00000000000000AdvancedClimateSystems-uModbus-4fd98ba/tests/system/responses/test_exception_responses.py000066400000000000000000000051471372201247100324520ustar00rootroot00000000000000import pytest import struct from functools import partial from ..validators import validate_response_mbap from umodbus.client import tcp @pytest.mark.parametrize('function_code, quantity', [ (1, 0), (2, 0), (3, 0), (4, 0), (1, 0x07D0 + 1), (2, 0x07D0 + 1), (3, 0x007D + 1), (4, 0x007D + 1), ]) def test_request_returning_invalid_data_value_error(sock, mbap, function_code, quantity): """ Validate response PDU of request returning excepetion response with error code 3. """ function_code, starting_address, quantity = (function_code, 0, quantity) adu = mbap + struct.pack('>BHH', function_code, starting_address, quantity) sock.send(adu) resp = sock.recv(1024) validate_response_mbap(mbap, resp) assert struct.unpack('>BB', resp[-2:]) == (0x80 + function_code, 3) @pytest.mark.parametrize('function', [ (partial(tcp.read_coils, 1, 9, 2)), (partial(tcp.read_discrete_inputs, 1, 9, 2)), (partial(tcp.read_holding_registers, 1, 9, 2)), (partial(tcp.read_input_registers, 1, 9, 2)), (partial(tcp.write_single_coil, 1, 11, 0)), (partial(tcp.write_single_register, 1, 11, 1337)), (partial(tcp.write_multiple_coils, 1, 9, [1, 1])), (partial(tcp.write_multiple_registers, 1, 9, [1337, 15])), ]) def test_request_returning_invalid_data_address_error(sock, function): """ Validate response PDU of request returning excepetion response with error code 2. """ adu = function() mbap = adu[:7] function_code = struct.unpack('>B', adu[7:8])[0] sock.send(adu) resp = sock.recv(1024) validate_response_mbap(mbap, resp) assert struct.unpack('>BB', resp[-2:]) == (0x80 + function_code, 2) @pytest.mark.parametrize('function', [ (partial(tcp.read_coils, 1, 666, 1)), (partial(tcp.read_discrete_inputs, 1, 666, 1)), (partial(tcp.read_holding_registers, 1, 666, 1)), (partial(tcp.read_input_registers, 1, 666, 1)), (partial(tcp.write_single_coil, 1, 666, 0)), (partial(tcp.write_single_register, 1, 666, 1337)), (partial(tcp.write_multiple_coils, 1, 666, [1])), (partial(tcp.write_multiple_registers, 1, 666, [1337])), ]) def test_request_returning_server_device_failure_error(sock, function): """ Validate response PDU of request returning excepetion response with error code 4. """ adu = function() mbap = adu[:7] function_code = struct.unpack('>B', adu[7:8])[0] sock.send(adu) resp = sock.recv(1024) validate_response_mbap(mbap, resp) assert struct.unpack('>BB', resp[-2:]) == (0x80 + function_code, 4) AdvancedClimateSystems-uModbus-4fd98ba/tests/system/responses/test_exception_rtu_responses.py000066400000000000000000000064661372201247100333510ustar00rootroot00000000000000import pytest import struct from functools import partial from ..validators import validate_response_mbap from umodbus.client.serial import rtu from umodbus.client.serial.redundancy_check import (get_crc, validate_crc, add_crc, CRCError) def test_no_response_for_request_with_invalid_crc(rtu_server): """ Test if server doesn't respond on a request with an invalid CRC. """ pdu = rtu.read_coils(1, 9, 2) adu = struct.pack('>B', 1) + pdu + struct.pack('>BB', 0, 0) rtu_server.serial_port.write(adu) with pytest.raises(CRCError): rtu_server.serve_once() @pytest.mark.parametrize('function_code, quantity', [ (1, 0), (2, 0), (3, 0), (4, 0), (1, 0x07D0 + 1), (2, 0x07D0 + 1), (3, 0x007D + 1), (4, 0x007D + 1), ]) def test_request_returning_invalid_data_value_error(rtu_server, function_code, quantity): """ Validate response PDU of request returning excepetion response with error code 3. """ starting_address = 0 slave_id = 1 adu = add_crc(struct.pack('>BBHH', slave_id, function_code, starting_address, quantity)) rtu_server.serial_port.write(adu) rtu_server.serve_once() resp = rtu_server.serial_port.read(rtu_server.serial_port.in_waiting) validate_crc(resp) assert struct.unpack('>BB', resp[1:-2]) == (0x80 + function_code, 3) @pytest.mark.parametrize('function', [ (partial(rtu.read_coils, 1, 9, 2)), (partial(rtu.read_discrete_inputs, 1, 9, 2)), (partial(rtu.read_holding_registers, 1, 9, 2)), (partial(rtu.read_input_registers, 1, 9, 2)), (partial(rtu.write_single_coil, 1, 11, 0)), (partial(rtu.write_single_register, 1, 11, 1337)), (partial(rtu.write_multiple_coils, 1, 9, [1, 1])), (partial(rtu.write_multiple_registers, 1, 9, [1337, 15])), ]) def test_request_returning_invalid_data_address_error(rtu_server, function): """ Validate response PDU of request returning excepetion response with error code 2. """ adu = function() function_code = struct.unpack('>B', adu[1:2])[0] rtu_server.serial_port.write(adu) rtu_server.serve_once() resp = rtu_server.serial_port.read(rtu_server.serial_port.in_waiting) validate_crc(resp) assert struct.unpack('>BB', resp[1:-2]) == (0x80 + function_code, 2) @pytest.mark.parametrize('function', [ (partial(rtu.read_coils, 1, 666, 1)), (partial(rtu.read_discrete_inputs, 1, 666, 1)), (partial(rtu.read_holding_registers, 1, 666, 1)), (partial(rtu.read_input_registers, 1, 666, 1)), (partial(rtu.write_single_coil, 1, 666, 0)), (partial(rtu.write_single_register, 1, 666, 1337)), (partial(rtu.write_multiple_coils, 1, 666, [1])), (partial(rtu.write_multiple_registers, 1, 666, [1337])), ]) def test_request_returning_server_device_failure_error(rtu_server, function): """ Validate response PDU of request returning excepetion response with error code 4. """ adu = function() function_code = struct.unpack('>B', adu[1:2])[0] rtu_server.serial_port.write(adu) rtu_server.serve_once() resp = rtu_server.serial_port.read(rtu_server.serial_port.in_waiting) validate_crc(resp) assert struct.unpack('>BB', resp[1:-2]) == (0x80 + function_code, 4) AdvancedClimateSystems-uModbus-4fd98ba/tests/system/responses/test_succesful_responses.py000066400000000000000000000043621372201247100324460ustar00rootroot00000000000000import pytest from umodbus import conf from umodbus.client import tcp @pytest.fixture(scope='module', autouse=True) def enable_signed_values(request): """ Use signed values when running tests it this module. """ tmp = conf.SIGNED_VALUES conf.SIGNED_VALUES = True def fin(): conf.SIGNED_VALUES = tmp request.addfinalizer(fin) @pytest.mark.parametrize('function', [ tcp.read_coils, tcp.read_discrete_inputs, ]) def test_response_on_single_bit_value_read_requests(sock, function): """ Validate response of a succesful Read Coils or Read Discrete Inputs request. """ slave_id, starting_address, quantity = (1, 0, 10) req_adu = function(slave_id, starting_address, quantity) assert tcp.send_message(req_adu, sock) == [0, 1, 0, 1, 0, 1, 0, 1, 0, 1] @pytest.mark.parametrize('function', [ tcp.read_holding_registers, tcp.read_input_registers, ]) def test_response_on_multi_bit_value_read_requests(sock, function): """ Validate response of a succesful Read Holding Registers or Read Input Registers request. """ slave_id, starting_address, quantity = (1, 0, 10) req_adu = function(slave_id, starting_address, quantity) assert tcp.send_message(req_adu, sock) ==\ [0, -1, -2, -3, -4, -5, -6, -7, -8, -9] @pytest.mark.parametrize('function, value', [ (tcp.write_single_coil, 1), (tcp.write_single_register, -1337), ]) def test_response_single_value_write_request(sock, function, value): """ Validate responde of succesful Read Single Coil and Read Single Register request. """ slave_id, starting_address, value = (1, 0, value) req_adu = function(slave_id, starting_address, value) assert tcp.send_message(req_adu, sock) == value @pytest.mark.parametrize('function, values', [ (tcp.write_multiple_coils, [1, 1]), (tcp.write_multiple_registers, [1337, 15]), ]) def test_response_multi_value_write_request(sock, function, values): """ Validate response of succesful Write Multiple Coils and Write Multiple Registers request. Both requests write 2 values, starting address is 0. """ slave_id, starting_address = (1, 0) req_adu = function(slave_id, starting_address, values) assert tcp.send_message(req_adu, sock) == 2 AdvancedClimateSystems-uModbus-4fd98ba/tests/system/responses/test_succesful_rtu_responses.py000066400000000000000000000047721372201247100333450ustar00rootroot00000000000000import pytest from umodbus import conf from umodbus.client.serial import rtu @pytest.fixture(scope='module', autouse=True) def enable_signed_values(request): """ Use signed values when running tests it this module. """ tmp = conf.SIGNED_VALUES conf.SIGNED_VALUES = True def fin(): conf.SIGNED_VALUES = tmp request.addfinalizer(fin) def send_message(adu, server): server.serial_port.write(adu) server.serve_once() response_adu = server.serial_port.read(server.serial_port.in_waiting) return rtu.parse_response_adu(response_adu, adu) @pytest.mark.parametrize('function', [ rtu.read_coils, rtu.read_discrete_inputs, ]) def test_response_on_single_bit_value_read_requests(rtu_server, function): """ Validate response of a succesful Read Coils or Read Discrete Inputs request. """ slave_id, starting_address, quantity = (1, 0, 10) req_adu = function(slave_id, starting_address, quantity) assert send_message(req_adu, rtu_server) == [0, 1, 0, 1, 0, 1, 0, 1, 0, 1] @pytest.mark.parametrize('function', [ rtu.read_holding_registers, rtu.read_input_registers, ]) def test_response_on_multi_bit_value_read_requests(rtu_server, function): """ Validate response of a succesful Read Holding Registers or Read Input Registers request. """ slave_id, starting_address, quantity = (1, 0, 10) req_adu = function(slave_id, starting_address, quantity) assert send_message(req_adu, rtu_server) ==\ [0, -1, -2, -3, -4, -5, -6, -7, -8, -9] @pytest.mark.parametrize('function, value', [ (rtu.write_single_coil, 0), (rtu.write_single_register, -1337), ]) def test_response_single_value_write_request(rtu_server, function, value): """ Validate responde of succesful Read Single Coil and Read Single Register request. """ slave_id, starting_address, quantity = (1, 0, value) req_adu = function(slave_id, starting_address, quantity) assert send_message(req_adu, rtu_server) == value @pytest.mark.parametrize('function, values', [ (rtu.write_multiple_coils, [1, 1]), (rtu.write_multiple_registers, [1337, 15]), ]) def test_response_multi_value_write_request(rtu_server, function, values): """ Validate response of succesful Write Multiple Coils and Write Multiple Registers request. Both requests write 2 values, starting address is 0. """ slave_id, starting_address = (1, 0) req_adu = function(slave_id, starting_address, values) assert send_message(req_adu, rtu_server) == 2 AdvancedClimateSystems-uModbus-4fd98ba/tests/system/route.py000066400000000000000000000017201372201247100244220ustar00rootroot00000000000000def bind_routes(server): server.route_map.add_rule(read_status, slave_ids=[1], function_codes=[1, 2], addresses=list(range(0, 10))) # NOQA server.route_map.add_rule(read_register, slave_ids=[1], function_codes=[3, 4], addresses=list(range(0, 10))) # NOQA server.route_map.add_rule(write_status, slave_ids=[1], function_codes=[5, 15], addresses=list(range(0, 10))) # NOQA server.route_map.add_rule(write_register, slave_ids=[1], function_codes=[6, 16], addresses=list(range(0, 10))) # NOQA server.route_map.add_rule(failure, slave_ids=[1], function_codes=[1, 2, 3, 4, 5, 6, 15, 16], addresses=[666]) # NOQA def read_status(slave_id, function_code, address): return address % 2 def read_register(slave_id, function_code, address): return -address def write_status(slave_id, function_code, address, value): pass def write_register(slave_id, function_code, address, value): pass def failure(*args, **kwargs): raise Exception AdvancedClimateSystems-uModbus-4fd98ba/tests/system/rtu_server.py000066400000000000000000000004521372201247100254650ustar00rootroot00000000000000from serial import serial_for_url from umodbus import conf from umodbus.server.serial import get_server from umodbus.server.serial.rtu import RTUServer from tests.system import route conf.SIGNED_VALUES = True s = serial_for_url('loop://') app = get_server(RTUServer, s) route.bind_routes(app) AdvancedClimateSystems-uModbus-4fd98ba/tests/system/tcp_server.py000066400000000000000000000005151372201247100254410ustar00rootroot00000000000000try: from socketserver import TCPServer except ImportError: from SocketServer import TCPServer from umodbus import conf from umodbus.server.tcp import get_server, RequestHandler from tests.system import route conf.SIGNED_VALUES = True app = get_server(TCPServer, ('localhost', 0), RequestHandler) route.bind_routes(app) AdvancedClimateSystems-uModbus-4fd98ba/tests/system/validators.py000066400000000000000000000043761372201247100254460ustar00rootroot00000000000000import struct def validate_transaction_id(request_mbap, response): """ Check if Transaction id in request and response is equal. """ assert struct.unpack('>H', request_mbap[:2]) == \ struct.unpack('>H', response[:2]) def validate_protocol_id(request_mbap, response): """ Check if Protocol id in request and response is equal. """ assert struct.unpack('>H', request_mbap[2:4]) == \ struct.unpack('>H', response[2:4]) def validate_length(response): """ Check if Length field contains actual length of response. """ assert struct.unpack('>H', response[4:6])[0] == len(response[6:]) def validate_unit_id(request_mbap, response): """ Check if Unit id in request and response is equal. """ assert struct.unpack('>B', request_mbap[6:7]) == \ struct.unpack('>B', response[6:7]) def validate_response_mbap(request_mbap, response): """ Validate if fields in response MBAP contain correct values. """ validate_transaction_id(request_mbap, response) validate_protocol_id(request_mbap, response) validate_length(response) validate_unit_id(request_mbap, response) def validate_function_code(request, response): """ Validate if Function code in request and response equal. """ assert struct.unpack('>B', request[7:8])[0] == \ struct.unpack('>B', response[7:8])[0] def validate_single_bit_value_byte_count(request, response): """ Check of byte count field contains actual byte count and if byte count matches with the amount of requests quantity. """ byte_count = struct.unpack('>B', response[8:9])[0] quantity = struct.unpack('>H', request[-2:])[0] expected_byte_count = quantity // 8 if quantity % 8 != 0: expected_byte_count = (quantity // 8) + 1 assert byte_count == len(response[9:]) assert byte_count == expected_byte_count def validate_multi_bit_value_byte_count(request, response): """ Check of byte count field contains actual byte count and if byte count matches with the amount of requests quantity. """ byte_count = struct.unpack('>B', response[8:9])[0] quantity = struct.unpack('>H', request[-2:])[0] expected_byte_count = quantity * 2 assert byte_count == len(response[9:]) assert byte_count == expected_byte_count AdvancedClimateSystems-uModbus-4fd98ba/tests/unit/000077500000000000000000000000001372201247100223455ustar00rootroot00000000000000AdvancedClimateSystems-uModbus-4fd98ba/tests/unit/__init__.py000066400000000000000000000000001372201247100244440ustar00rootroot00000000000000AdvancedClimateSystems-uModbus-4fd98ba/tests/unit/client/000077500000000000000000000000001372201247100236235ustar00rootroot00000000000000AdvancedClimateSystems-uModbus-4fd98ba/tests/unit/client/__init__.py000066400000000000000000000000001372201247100257220ustar00rootroot00000000000000AdvancedClimateSystems-uModbus-4fd98ba/tests/unit/client/serial/000077500000000000000000000000001372201247100251025ustar00rootroot00000000000000AdvancedClimateSystems-uModbus-4fd98ba/tests/unit/client/serial/__init__.py000066400000000000000000000000001372201247100272010ustar00rootroot00000000000000AdvancedClimateSystems-uModbus-4fd98ba/tests/unit/client/serial/test_redundancy_check.py000066400000000000000000000013711372201247100320060ustar00rootroot00000000000000import struct import pytest from umodbus.client.serial.redundancy_check import (get_crc, validate_crc, CRCError) def test_get_crc(): """ Test if correct CRC is calculated. """ # Values are equal to those used in example in MODBUS over serial line # specification and implementation guide V1.02, chapter 6.2.2. assert struct.unpack('H', mbap[:2])[0] protocol_id = struct.unpack('>H', mbap[2:4])[0] length = struct.unpack('>H', mbap[4:6])[0] unit_id = struct.unpack('>B', mbap[6:])[0] assert len(mbap) == 7 assert 0 <= transaction_id <= 65536 assert protocol_id == 0 assert length == len(pdu) + 1 assert unit_id == slave_id AdvancedClimateSystems-uModbus-4fd98ba/tests/unit/conftest.py000066400000000000000000000006361372201247100245510ustar00rootroot00000000000000import pytest from umodbus import conf from umodbus.config import Config @pytest.fixture(scope='module', autouse=True) def enable_signed_values(request): """ Use signed values when running tests it this module. """ tmp = conf.SIGNED_VALUES conf.SIGNED_VALUES = False def fin(): conf.SIGNED_VALUES = tmp request.addfinalizer(fin) @pytest.fixture def config(): return Config() AdvancedClimateSystems-uModbus-4fd98ba/tests/unit/server/000077500000000000000000000000001372201247100236535ustar00rootroot00000000000000AdvancedClimateSystems-uModbus-4fd98ba/tests/unit/server/__init__.py000066400000000000000000000000001372201247100257520ustar00rootroot00000000000000AdvancedClimateSystems-uModbus-4fd98ba/tests/unit/server/serial/000077500000000000000000000000001372201247100251325ustar00rootroot00000000000000AdvancedClimateSystems-uModbus-4fd98ba/tests/unit/server/serial/test_init.py000066400000000000000000000011431372201247100275050ustar00rootroot00000000000000import pytest from umodbus.server.serial import AbstractSerialServer @pytest.fixture def abstract_serial_server(): return AbstractSerialServer() def test_abstract_serial_server_get_meta_data(abstract_serial_server): """ Test if meta data is correctly extracted from request. """ assert abstract_serial_server.get_meta_data(b'\x01x\02\x03') ==\ {'unit_id': 1} def test_abract_serial_server_shutdown(abstract_serial_server): assert abstract_serial_server._shutdown_request is False abstract_serial_server.shutdown() assert abstract_serial_server._shutdown_request is True AdvancedClimateSystems-uModbus-4fd98ba/tests/unit/server/serial/test_rtu.py000066400000000000000000000020001372201247100273450ustar00rootroot00000000000000import pytest from serial import Serial, serial_for_url from umodbus.server.serial.rtu import RTUServer, get_char_size @pytest.fixture def rtu_server(): return RTUServer() def test_get_char_size(): """ Test if correct char size is calculated. """ assert get_char_size(11) == 1 assert get_char_size(19201) == 0.0005 def test_rtu_server_create_response_adu(rtu_server): assert rtu_server.create_response_adu({'unit_id': 1}, b'') == b'\x01~\x80' def test_rtu_server_serial_port(rtu_server): """" Test if RTUServer.serial_port sets correct timeout and inter_byte_timeout. """ serial_port = Serial(baudrate=19201) rtu_server.serial_port = serial_port assert rtu_server.serial_port.timeout == 0.00175 assert rtu_server.serial_port.inter_byte_timeout == 0.00075 def test_rtu_server_send_empty_message(rtu_server): rtu_server.serial_port = serial_for_url('loop://') rtu_server.serial_port.write(b'') with pytest.raises(ValueError): rtu_server.serve_once() AdvancedClimateSystems-uModbus-4fd98ba/tests/unit/server/test_tcp.py000066400000000000000000000036651372201247100260640ustar00rootroot00000000000000""" The module umodbus.server is mainly covered through system tests. Only those parts which can't be tested by system tests should be tested using unit tests. """ import struct import pytest from umodbus.exceptions import ServerDeviceFailureError from umodbus.client.tcp import read_coils from umodbus.server.tcp import RequestHandler @pytest.fixture def meta_data(): return { 'transaction_id': 1337, 'protocol_id': 0, 'length': 1, 'unit_id': 5, } @pytest.fixture def mbap_header(): transaction_id = 1337 protocol_id = 0 length = 1 unit_id = 5 return struct.pack('>HHHB', transaction_id, protocol_id, length, unit_id) @pytest.fixture def request_handler(monkeypatch): # handle() is called when after creating a RequestHandler. This # causes an error because it wants to read from a socket. Mock it out so # this error doesn't occur. monkeypatch.setattr(RequestHandler, 'handle', lambda _: None) return RequestHandler(None, None, None) def test_handle_raising_exception(): """ Test tests RequestHandler.handle() which is called when an instance of RequestHandler is created. This method should reraise exception if one occurs. """ with pytest.raises(AttributeError): RequestHandler(None, None, None) def test_request_handler_get_meta_data(request_handler, mbap_header, meta_data): assert request_handler.get_meta_data(mbap_header) == meta_data def test_request_handler_get_meta_data_raising_error(request_handler): with pytest.raises(ServerDeviceFailureError): request_handler.get_meta_data(b'') def def_test_get_request_pdu(request_handler, mbap_header): pdu = read_coils(1, 1, 1) assert request_handler.get_request_pdu(mbap_header + pdu) == pdu def test_response_adu(request_handler, mbap_header, meta_data): assert len(request_handler.create_response_adu(meta_data, b'')) == 7 AdvancedClimateSystems-uModbus-4fd98ba/tests/unit/test_config.py000066400000000000000000000011431372201247100252220ustar00rootroot00000000000000class TestConfig: def test_defaults(self, config): """ Test whether defaults configuration values are correct. """ assert config.SINGLE_BIT_VALUE_FORMAT_CHARACTER == 'B' assert config.MULTI_BIT_VALUE_FORMAT_CHARACTER == 'H' assert not config.SIGNED_VALUES def test_multi_bit_value_signed(self, config): """ Test if MULTI_BIT_VALUE_FORMAT_CHARACTER changes when setting signedness. """ assert config.MULTI_BIT_VALUE_FORMAT_CHARACTER == 'H' config.SIGNED_VALUES = True assert config.MULTI_BIT_VALUE_FORMAT_CHARACTER == 'h' AdvancedClimateSystems-uModbus-4fd98ba/tests/unit/test_functions.py000066400000000000000000000254121372201247100257720ustar00rootroot00000000000000import struct import pytest try: # Mock has been added to stdlib in Python 3.3. from unittest.mock import MagicMock, call except ImportError: from mock import MagicMock, call from umodbus.route import Map from umodbus.exceptions import (IllegalFunctionError, IllegalDataAddressError, IllegalDataValueError, ServerDeviceFailureError, AcknowledgeError, ServerDeviceBusyError, MemoryParityError, GatewayPathUnavailableError, GatewayTargetDeviceFailedToRespondError) from umodbus.functions import (create_function_from_response_pdu, create_function_from_request_pdu, ReadCoils, ReadDiscreteInputs, ReadHoldingRegisters, ReadInputRegisters, WriteSingleCoil, WriteSingleRegister, WriteMultipleCoils, WriteMultipleRegisters) @pytest.fixture def read_coils(): instance = ReadCoils() instance.starting_address = 100 instance.quantity = 3 return instance @pytest.fixture def read_discrete_inputs(): instance = ReadDiscreteInputs() instance.starting_address = 0 instance.quantity = 2 return instance @pytest.fixture def read_holding_registers(): instance = ReadHoldingRegisters() instance.starting_address = 100 instance.quantity = 4 return instance @pytest.fixture def read_input_registers(): instance = ReadInputRegisters() instance.starting_address = 50 instance.quantity = 2 return instance @pytest.fixture def write_single_coil(): instance = WriteSingleCoil() instance.address = 100 instance.value = 1 return instance @pytest.fixture def write_single_register(): instance = WriteSingleRegister() instance.address = 200 instance.value = 18 return instance @pytest.fixture def write_multiple_coils(): instance = WriteMultipleCoils() instance.starting_address = 100 instance.values = [1, 0] return instance @pytest.fixture def write_multiple_registers(): instance = WriteMultipleRegisters() instance.starting_address = 50 instance.values = [1337, 15, 128] return instance @pytest.fixture def route_map(): return Map() @pytest.mark.parametrize('pdu,cls', [ (b'\x01\x00d\x00\x03', ReadCoils), (b'\x02\x00d\x00\x03', ReadDiscreteInputs), (b'\x03\x00d\x00\x03', ReadHoldingRegisters), (b'\x04\x00d\x00\x03', ReadInputRegisters), (b'\x05\x00d\x00\x00', WriteSingleCoil), (b'\x06\x00d\x00\x00', WriteSingleRegister), (b'\x0f\x00d\x00\x03\x01\x04', WriteMultipleCoils), (b'\x10\x00d\x00\x01\x02\x00\x04', WriteMultipleRegisters), ]) def test_create_function_from_request_pdu(pdu, cls): assert isinstance(create_function_from_request_pdu(pdu), cls) @pytest.mark.parametrize('error_code, exception_class', [ (1, IllegalFunctionError), (2, IllegalDataAddressError), (3, IllegalDataValueError), (4, ServerDeviceFailureError), (5, AcknowledgeError), (6, ServerDeviceBusyError), (8, MemoryParityError), (10, GatewayPathUnavailableError), (11, GatewayTargetDeviceFailedToRespondError), ]) def test_create_from_response_pdu_raising_exception(error_code, exception_class): """ Test if correct exception is raied when response PDU contains error response. """ resp_pdu = struct.pack('>BB', 81, error_code) with pytest.raises(exception_class): create_function_from_response_pdu(resp_pdu) def test_create_function_from_request_pdu_raising_illegal_function_error(): """ Calling function with PDU containing invalid function code should result in an IllegalFunctionError. """ with pytest.raises(IllegalFunctionError): create_function_from_request_pdu(b'\x00') def test_read_coils_class_attributes(): assert ReadCoils.function_code == 1 assert ReadCoils.max_quantity == 2000 def test_read_coils_with_invalid_attributes(read_coils): with pytest.raises(IllegalDataValueError): read_coils.quantity = 2001 def test_read_coils_request_pdu(read_coils): instance = ReadCoils.create_from_request_pdu(read_coils.request_pdu) assert instance.starting_address == 100 assert instance.quantity == 3 def test_read_coils_response_pdu(read_coils): response_pdu = read_coils.create_response_pdu([0, 1, 1]) instance = ReadCoils.create_from_response_pdu(response_pdu, read_coils.request_pdu) # NOQA assert instance.data == [0, 1, 1] def test_read_discrete_inputs_class_attributes(): assert ReadDiscreteInputs.function_code == 2 assert ReadDiscreteInputs.max_quantity == 2000 def test_read_discrete_inputs_with_invalid_attributes(read_discrete_inputs): with pytest.raises(IllegalDataValueError): read_discrete_inputs.quantity = 2001 def test_read_discrete_inputs_request_pdu(read_discrete_inputs): instance = ReadDiscreteInputs.create_from_request_pdu(read_discrete_inputs.request_pdu) # NOQA assert instance.starting_address == 0 assert instance.quantity == 2 def test_read_discrete_inputs_response_pdu(read_discrete_inputs): response_pdu = read_discrete_inputs.create_response_pdu([1, 0]) instance = ReadDiscreteInputs.create_from_response_pdu(response_pdu, read_discrete_inputs.request_pdu) # NOQA assert instance.data == [1, 0] def test_read_holding_registers_class_attributes(): assert ReadHoldingRegisters.function_code == 3 assert ReadHoldingRegisters.max_quantity == 125 def test_read_holding_registers_with_invalid_attributes(read_holding_registers): # NOQA with pytest.raises(IllegalDataValueError): read_holding_registers.quantity = 126 def test_read_holding_registers_request_pdu(read_holding_registers): instance = ReadHoldingRegisters.create_from_request_pdu(read_holding_registers.request_pdu) # NOQA assert instance.starting_address == 100 assert instance.quantity == 4 def test_read_holding_registers_response_pdu(read_holding_registers): response_pdu =\ read_holding_registers.create_response_pdu([1337, 17, 21, 18]) instance = ReadHoldingRegisters.create_from_response_pdu(response_pdu, read_holding_registers.request_pdu) # NOQA assert instance.data == [1337, 17, 21, 18] def test_read_input_registers_class_attributes(): assert ReadInputRegisters.function_code == 4 assert ReadInputRegisters.max_quantity == 125 def test_read_input_registers_with_invalid_attributes(read_input_registers): # NOQA with pytest.raises(IllegalDataValueError): read_input_registers.quantity = 126 def test_read_input_registers_request_pdu(read_input_registers): instance = ReadInputRegisters.create_from_request_pdu(read_input_registers.request_pdu) # NOQA assert instance.starting_address == 50 assert instance.quantity == 2 def test_read_input_registers_response_pdu(read_input_registers): response_pdu = read_input_registers.create_response_pdu([994, 1100]) instance = ReadInputRegisters.create_from_response_pdu(response_pdu, read_input_registers.request_pdu) # NOQA assert instance.data == [994, 1100] def test_write_single_coil_class_attributes(): assert WriteSingleCoil.function_code == 5 def test_write_single_coil_with_invalid_attributes(write_single_coil): with pytest.raises(IllegalDataValueError): write_single_coil.value = 5 def test_write_single_coil_request_pdu(write_single_coil): instance = WriteSingleCoil.create_from_request_pdu(write_single_coil.request_pdu) # NOQA assert instance.address == 100 assert instance.value == 1 def test_write_single_coil_response_pdu(write_single_coil): response_pdu = write_single_coil.create_response_pdu() instance = WriteSingleCoil.create_from_response_pdu(response_pdu) assert instance.address == 100 assert instance.data == 1 def test_write_single_registers_class_attributes(): assert WriteSingleRegister.function_code == 6 def test_write_single_register_with_invalid_attributes(write_single_register): with pytest.raises(IllegalDataValueError): write_single_register.value = -1 def test_write_single_register_request_pdu(write_single_register): instance = WriteSingleRegister.create_from_request_pdu(write_single_register.request_pdu) # NOQA assert instance.address == 200 assert instance.value == 18 def test_write_single_register_response_pdu(write_single_register): response_pdu = write_single_register.create_response_pdu() instance = WriteSingleRegister.create_from_response_pdu(response_pdu) assert instance.address == 200 assert instance.data == 18 def test_write_multiple_coils_class_attributes(): WriteMultipleCoils.function_code == 15 def test_write_multiple_coils_invalid_attributes(write_multiple_coils): with pytest.raises(IllegalDataValueError): write_multiple_coils.values = [] with pytest.raises(IllegalDataValueError): write_multiple_coils.values = [5] def test_write_multiple_coils_request_pdu(write_multiple_coils): instance = WriteMultipleCoils.create_from_request_pdu(write_multiple_coils.request_pdu) # NOQA assert instance.starting_address == 100 assert instance.values == [1, 0] def test_write_multiple_coils_response_pdu(write_multiple_coils): response_pdu = write_multiple_coils.create_response_pdu() instance = WriteMultipleCoils.create_from_response_pdu(response_pdu) assert instance.starting_address == 100 assert instance.data == 2 def test_write_multiple_registers_class_attributes(): WriteMultipleRegisters.function_code == 16 def test_write_multiple_registers_invalid_attributes(write_multiple_registers): with pytest.raises(IllegalDataValueError): write_multiple_registers.values = [] with pytest.raises(IllegalDataValueError): write_multiple_registers.values = [-1] def test_write_multiple_registers_request_pdu(write_multiple_registers): instance = WriteMultipleRegisters.create_from_request_pdu(write_multiple_registers.request_pdu) # NOQA assert instance.starting_address == 50 assert instance.values == [1337, 15, 128] def test_write_multiple_registers_response_pdu(write_multiple_registers): response_pdu = write_multiple_registers.create_response_pdu() instance = WriteMultipleRegisters.create_from_response_pdu(response_pdu) assert instance.starting_address == 50 assert instance.data == 3 def test_create_function_from_response_pdu(): read_coils = ReadCoils() read_coils.starting_address = 1 read_coils.quantity = 9 req_pdu = read_coils.request_pdu resp_pdu = struct.pack('>BBB', 1, 1, 3) assert isinstance(create_function_from_response_pdu(resp_pdu, req_pdu), ReadCoils) AdvancedClimateSystems-uModbus-4fd98ba/tests/unit/test_route.py000066400000000000000000000023451372201247100251200ustar00rootroot00000000000000import pytest from umodbus.route import DataRule endpoint = lambda slave_id, function_code, address: 0 def test_basic_route(): rule = DataRule(endpoint, slave_ids=[1], function_codes=[1], addresses=[1]) assert rule.match(slave_id=1, function_code=1, address=1) assert not rule.match(slave_id=0, function_code=1, address=1) assert not rule.match(slave_id=1, function_code=0, address=1) assert not rule.match(slave_id=1, function_code=1, address=0) def test_other_iterables(): # Other iterable types should work, not just lists rule = DataRule(endpoint, slave_ids=set([1]), function_codes=[1], addresses=[1]) assert rule.match(slave_id=1, function_code=1, address=1) def test_wildcard_slave_id(): rule = DataRule(endpoint, slave_ids=None, function_codes=[1], addresses=[1]) assert rule.match(slave_id=1, function_code=1, address=1) def test_wildcard_function_code(): rule = DataRule(endpoint, slave_ids=[1], function_codes=None, addresses=[1]) assert rule.match(slave_id=1, function_code=1, address=1) def test_wildcard_address(): rule = DataRule(endpoint, slave_ids=[1], function_codes=[1], addresses=None) assert rule.match(slave_id=1, function_code=1, address=1) AdvancedClimateSystems-uModbus-4fd98ba/tests/unit/test_tcp.py000066400000000000000000000021461372201247100245470ustar00rootroot00000000000000import struct from umodbus.client.tcp import _create_request_adu, _create_mbap_header def validate_mbap_fields(mbap, slave_id, pdu): """ Check if fields in MBAP header contain expected values. """ transaction_id = struct.unpack('>H', mbap[:2])[0] protocol_id = struct.unpack('>H', mbap[2:4])[0] length = struct.unpack('>H', mbap[4:6])[0] unit_id = struct.unpack('>B', mbap[6:])[0] assert len(mbap) == 7 assert 0 <= transaction_id <= 65536 assert protocol_id == 0 assert length == len(pdu) + 1 assert unit_id == slave_id def test_create_request_adu(): """ Validate MBAP header of ADU and check if ADU contains correct PDU. """ pdu = b'\x01' slave_id = 1 adu = _create_request_adu(slave_id, pdu) # 9 is length MBAP (7 bytes) with length of PDU (1 byte) assert len(adu) == 8 assert adu[7:] == pdu validate_mbap_fields(adu[:7], slave_id, pdu) def test_create_mbap_header(): """ Validate fields of MBAP header. """ pdu = b'\x01x02' slave_id = 1 mbap = _create_mbap_header(slave_id, pdu) validate_mbap_fields(mbap, slave_id, pdu) AdvancedClimateSystems-uModbus-4fd98ba/tests/unit/test_utils.py000066400000000000000000000025371372201247100251250ustar00rootroot00000000000000import sys import logging from logging import getLogger from umodbus.utils import (log_to_stream, unpack_mbap, pack_mbap, pack_exception_pdu, get_function_code_from_request_pdu) def test_log_to_stream(): """ Test if handler is added correctly. """ log = getLogger('uModbus') # NullHandler is attached. assert len(log.handlers) == 1 log_to_stream() assert len(log.handlers) == 2 handler = log.handlers[1] assert handler.stream == sys.stderr assert handler.level == logging.NOTSET def test_unpack_mbap(): """ MBAP should contain correct values for Transaction identifier, Protocol identifier, Length and Unit identifer. """ assert unpack_mbap(b'\x00\x08\x00\x00\x00\x06\x01') == (8, 0, 6, 1) def test_pack_mbap(): """ Byte array should contain correct encoding of Transaction identifier, Protocol identifier, Length and Unit identifier. """ assert pack_mbap(8, 0, 6, 1) == b'\x00\x08\x00\x00\x00\x06\x01' def test_pack_exception_pdu(): """ Exception PDU should correct encoding of error code and function code. """ assert pack_exception_pdu(1, 1) == b'\x81\x01' def test_get_function_code_from_request_pdu(): """ Get correct function code from PDU. """ assert get_function_code_from_request_pdu(b'\x01\x00d\x00\x03') == 1 AdvancedClimateSystems-uModbus-4fd98ba/umodbus/000077500000000000000000000000001372201247100217025ustar00rootroot00000000000000AdvancedClimateSystems-uModbus-4fd98ba/umodbus/__init__.py000066400000000000000000000002311372201247100240070ustar00rootroot00000000000000from logging import getLogger, NullHandler log = getLogger('uModbus') log.addHandler(NullHandler()) from .config import Config # NOQA conf = Config() AdvancedClimateSystems-uModbus-4fd98ba/umodbus/client/000077500000000000000000000000001372201247100231605ustar00rootroot00000000000000AdvancedClimateSystems-uModbus-4fd98ba/umodbus/client/__init__.py000066400000000000000000000000001372201247100252570ustar00rootroot00000000000000AdvancedClimateSystems-uModbus-4fd98ba/umodbus/client/serial/000077500000000000000000000000001372201247100244375ustar00rootroot00000000000000AdvancedClimateSystems-uModbus-4fd98ba/umodbus/client/serial/__init__.py000066400000000000000000000000001372201247100265360ustar00rootroot00000000000000AdvancedClimateSystems-uModbus-4fd98ba/umodbus/client/serial/redundancy_check.py000066400000000000000000000034271372201247100303100ustar00rootroot00000000000000""" CRC is calculated over slave id + PDU. Most code is taken from: https://github.com/pyhys/minimalmodbus/blob/e99f4d74c83258c6039073082955ac9bed3f2155/minimalmodbus.py # NOQA """ import struct def generate_look_up_table(): """ Generate look up table. :return: List """ poly = 0xA001 table = [] for index in range(256): data = index << 1 crc = 0 for _ in range(8, 0, -1): data >>= 1 if (data ^ crc) & 0x0001: crc = (crc >> 1) ^ poly else: crc >>= 1 table.append(crc) return table look_up_table = generate_look_up_table() def get_crc(msg): """ Return CRC of 2 byte for message. >>> assert get_crc(b'\x02\x07') == struct.unpack('> 8) ^ look_up_table[(register ^ val) & 0xFF] # CRC is little-endian! return struct.pack(' `b\x01\x00d` .. code-block:: python >>> # Read coils, starting from coil 100 for the length of 3 coils. >>> adu = b'\\x01\\x01\\x00d\\x00\\x03=\\xd4' The lenght of this ADU is 8 bytes:: >>> len(adu) 8 """ import struct from umodbus.client.serial.redundancy_check import get_crc, validate_crc from umodbus.functions import (create_function_from_response_pdu, expected_response_pdu_size_from_request_pdu, pdu_to_function_code_or_raise_error, ReadCoils, ReadDiscreteInputs, ReadHoldingRegisters, ReadInputRegisters, WriteSingleCoil, WriteSingleRegister, WriteMultipleCoils, WriteMultipleRegisters) from umodbus.utils import recv_exactly def _create_request_adu(slave_id, req_pdu): """ Return request ADU for Modbus RTU. :param slave_id: Slave id. :param req_pdu: Byte array with PDU. :return: Byte array with ADU. """ first_part_adu = struct.pack('>B', slave_id) + req_pdu return first_part_adu + get_crc(first_part_adu) def read_coils(slave_id, starting_address, quantity): """ Return ADU for Modbus function code 01: Read Coils. :param slave_id: Number of slave. :return: Byte array with ADU. """ function = ReadCoils() function.starting_address = starting_address function.quantity = quantity return _create_request_adu(slave_id, function.request_pdu) def read_discrete_inputs(slave_id, starting_address, quantity): """ Return ADU for Modbus function code 02: Read Discrete Inputs. :param slave_id: Number of slave. :return: Byte array with ADU. """ function = ReadDiscreteInputs() function.starting_address = starting_address function.quantity = quantity return _create_request_adu(slave_id, function.request_pdu) def read_holding_registers(slave_id, starting_address, quantity): """ Return ADU for Modbus function code 03: Read Holding Registers. :param slave_id: Number of slave. :return: Byte array with ADU. """ function = ReadHoldingRegisters() function.starting_address = starting_address function.quantity = quantity return _create_request_adu(slave_id, function.request_pdu) def read_input_registers(slave_id, starting_address, quantity): """ Return ADU for Modbus function code 04: Read Input Registers. :param slave_id: Number of slave. :return: Byte array with ADU. """ function = ReadInputRegisters() function.starting_address = starting_address function.quantity = quantity return _create_request_adu(slave_id, function.request_pdu) def write_single_coil(slave_id, address, value): """ Return ADU for Modbus function code 05: Write Single Coil. :param slave_id: Number of slave. :return: Byte array with ADU. """ function = WriteSingleCoil() function.address = address function.value = value return _create_request_adu(slave_id, function.request_pdu) def write_single_register(slave_id, address, value): """ Return ADU for Modbus function code 06: Write Single Register. :param slave_id: Number of slave. :return: Byte array with ADU. """ function = WriteSingleRegister() function.address = address function.value = value return _create_request_adu(slave_id, function.request_pdu) def write_multiple_coils(slave_id, starting_address, values): """ Return ADU for Modbus function code 15: Write Multiple Coils. :param slave_id: Number of slave. :return: Byte array with ADU. """ function = WriteMultipleCoils() function.starting_address = starting_address function.values = values return _create_request_adu(slave_id, function.request_pdu) def write_multiple_registers(slave_id, starting_address, values): """ Return ADU for Modbus function code 16: Write Multiple Registers. :param slave_id: Number of slave. :return: Byte array with ADU. """ function = WriteMultipleRegisters() function.starting_address = starting_address function.values = values return _create_request_adu(slave_id, function.request_pdu) def parse_response_adu(resp_adu, req_adu=None): """ Parse response ADU and return response data. Some functions require request ADU to fully understand request ADU. :param resp_adu: Resonse ADU. :param req_adu: Request ADU, default None. :return: Response data. """ resp_pdu = resp_adu[1:-2] validate_crc(resp_adu) req_pdu = None if req_adu is not None: req_pdu = req_adu[1:-2] function = create_function_from_response_pdu(resp_pdu, req_pdu) return function.data def raise_for_exception_adu(resp_adu): """ Check a response ADU for error :param resp_adu: Response ADU. :raises ModbusError: When a response contains an error code. """ resp_pdu = resp_adu[1:-2] pdu_to_function_code_or_raise_error(resp_pdu) def send_message(adu, serial_port): """ Send ADU over serial to to server and return parsed response. :param adu: Request ADU. :param sock: Serial port instance. :return: Parsed response from server. """ serial_port.write(adu) serial_port.flush() # Check exception ADU (which is shorter than all other responses) first. exception_adu_size = 5 response_error_adu = recv_exactly(serial_port.read, exception_adu_size) raise_for_exception_adu(response_error_adu) expected_response_size = \ expected_response_pdu_size_from_request_pdu(adu[1:-2]) + 3 response_remainder = recv_exactly( serial_port.read, expected_response_size - exception_adu_size) return parse_response_adu(response_error_adu + response_remainder, adu) AdvancedClimateSystems-uModbus-4fd98ba/umodbus/client/tcp.py000066400000000000000000000215751372201247100243320ustar00rootroot00000000000000""" .. note:: This section is based on `MODBUS Messaging on TCP/IP Implementation Guide V1.0b`_. The Application Data Unit (ADU) for Modbus messages carried over a TCP/IP are build out of two components: a MBAP header and a PDU. The Modbus Application Header (MBAP) is what makes Modbus TCP/IP requests and responses different from their counterparts send over a serial line. Below the components of the Modbus TCP/IP are listed together with their size in bytes: +---------------+-----------------+ | **Component** | **Size** (bytes)| +---------------+-----------------+ | MBAP Header | 7 | +---------------+-----------------+ | PDU | N | +---------------+-----------------+ Below you see a hexadecimal presentation of request over TCP/IP with Modbus function code 1. It requests data of slave with 1, starting at coil 100, for the length of 3 coils: .. Note: the backslash in the bytes below are escaped using an extra back slash. Without escaping the bytes aren't printed correctly in the HTML output of this docs. To work with the bytes in Python you need to remove the escape sequences. `b'\\x01\\x00d` -> `b\x01\x00d` .. code-block:: python >>> # Read coils, starting from coil 100 for the length of 3 coils. >>> adu = b'\\x00\\x08\\x00\\x00\\x00\\x06\\x01\\x01\\x00d\\x00\\x03' The length of the ADU is 12 bytes:: >>> len(adu) 12 The MBAP header is 7 bytes long:: >>> mbap = adu[:7] >>> mbap b'\\x00\\x08\\x00\\x00\\x00\\x06\\x01' The MBAP header contains the following fields: +------------------------+--------------------+--------------------------------------+ | **Field** | **Length** (bytes) | **Description** | +------------------------+--------------------+--------------------------------------+ | Transaction identifier | 2 | Identification of a | | | | Modbus request/response transaction. | +------------------------+--------------------+--------------------------------------+ | Protocol identifier | 2 | Protocol ID, is 0 for Modbus. | +------------------------+--------------------+--------------------------------------+ | Length | 2 | Number of following bytes | +------------------------+--------------------+--------------------------------------+ | Unit identifier | 1 | Identification of a | | | | remote slave | +------------------------+--------------------+--------------------------------------+ When unpacked, these fields have the following values:: >>> transaction_id = mbap[:2] >>> transaction_id b'\\x00\\x08' >>> protocol_id = mbap[2:4] >>> protocol_id b'\\x00\\x00' >>> length = mbap[4:6] >>> length b'\\x00\\x06' >>> unit_id = mbap[6:] >>> unit_id b'\\0x01' The request in words: a request with Transaction ID 8 for slave 1. The request uses Protocol ID 0, which is the Modbus protocol. The length of the bytes after the Length field is 6 bytes. These 6 bytes are Unit Identifier (1 byte) + PDU (5 bytes). """ import struct from random import randint from umodbus.functions import (create_function_from_response_pdu, expected_response_pdu_size_from_request_pdu, pdu_to_function_code_or_raise_error, ReadCoils, ReadDiscreteInputs, ReadHoldingRegisters, ReadInputRegisters, WriteSingleCoil, WriteSingleRegister, WriteMultipleCoils, WriteMultipleRegisters) from umodbus.utils import recv_exactly def _create_request_adu(slave_id, pdu): """ Create MBAP header and combine it with PDU to return ADU. :param slave_id: Number of slave. :param pdu: Byte array with PDU. :return: Byte array with ADU. """ return _create_mbap_header(slave_id, pdu) + pdu def _create_mbap_header(slave_id, pdu): """ Return byte array with MBAP header for PDU. :param slave_id: Number of slave. :param pdu: Byte array with PDU. :return: Byte array of 7 bytes with MBAP header. """ # 65535 = (2**16)-1 aka maximum number that fits in 2 bytes. transaction_id = randint(0, 65535) length = len(pdu) + 1 return struct.pack('>HHHB', transaction_id, 0, length, slave_id) def read_coils(slave_id, starting_address, quantity): """ Return ADU for Modbus function code 01: Read Coils. :param slave_id: Number of slave. :return: Byte array with ADU. """ function = ReadCoils() function.starting_address = starting_address function.quantity = quantity return _create_request_adu(slave_id, function.request_pdu) def read_discrete_inputs(slave_id, starting_address, quantity): """ Return ADU for Modbus function code 02: Read Discrete Inputs. :param slave_id: Number of slave. :return: Byte array with ADU. """ function = ReadDiscreteInputs() function.starting_address = starting_address function.quantity = quantity return _create_request_adu(slave_id, function.request_pdu) def read_holding_registers(slave_id, starting_address, quantity): """ Return ADU for Modbus function code 03: Read Holding Registers. :param slave_id: Number of slave. :return: Byte array with ADU. """ function = ReadHoldingRegisters() function.starting_address = starting_address function.quantity = quantity return _create_request_adu(slave_id, function.request_pdu) def read_input_registers(slave_id, starting_address, quantity): """ Return ADU for Modbus function code 04: Read Input Registers. :param slave_id: Number of slave. :return: Byte array with ADU. """ function = ReadInputRegisters() function.starting_address = starting_address function.quantity = quantity return _create_request_adu(slave_id, function.request_pdu) def write_single_coil(slave_id, address, value): """ Return ADU for Modbus function code 05: Write Single Coil. :param slave_id: Number of slave. :return: Byte array with ADU. """ function = WriteSingleCoil() function.address = address function.value = value return _create_request_adu(slave_id, function.request_pdu) def write_single_register(slave_id, address, value): """ Return ADU for Modbus function code 06: Write Single Register. :param slave_id: Number of slave. :return: Byte array with ADU. """ function = WriteSingleRegister() function.address = address function.value = value return _create_request_adu(slave_id, function.request_pdu) def write_multiple_coils(slave_id, starting_address, values): """ Return ADU for Modbus function code 15: Write Multiple Coils. :param slave_id: Number of slave. :return: Byte array with ADU. """ function = WriteMultipleCoils() function.starting_address = starting_address function.values = values return _create_request_adu(slave_id, function.request_pdu) def write_multiple_registers(slave_id, starting_address, values): """ Return ADU for Modbus function code 16: Write Multiple Registers. :param slave_id: Number of slave. :return: Byte array with ADU. """ function = WriteMultipleRegisters() function.starting_address = starting_address function.values = values return _create_request_adu(slave_id, function.request_pdu) def parse_response_adu(resp_adu, req_adu=None): """ Parse response ADU and return response data. Some functions require request ADU to fully understand request ADU. :param resp_adu: Resonse ADU. :param req_adu: Request ADU, default None. :return: Response data. """ resp_pdu = resp_adu[7:] function = create_function_from_response_pdu(resp_pdu, req_adu) return function.data def raise_for_exception_adu(resp_adu): """ Check a response ADU for error :param resp_adu: Response ADU. :raises ModbusError: When a response contains an error code. """ resp_pdu = resp_adu[7:] pdu_to_function_code_or_raise_error(resp_pdu) def send_message(adu, sock): """ Send ADU over socket to to server and return parsed response. :param adu: Request ADU. :param sock: Socket instance. :return: Parsed response from server. """ sock.sendall(adu) # Check exception ADU (which is shorter than all other responses) first. exception_adu_size = 9 response_error_adu = recv_exactly(sock.recv, exception_adu_size) raise_for_exception_adu(response_error_adu) expected_response_size = \ expected_response_pdu_size_from_request_pdu(adu[7:]) + 7 response_remainder = recv_exactly( sock.recv, expected_response_size - exception_adu_size) return parse_response_adu(response_error_adu + response_remainder, adu) AdvancedClimateSystems-uModbus-4fd98ba/umodbus/config.py000066400000000000000000000054741372201247100235330ustar00rootroot00000000000000import os class Config(object): """ Class to hold global configuration. """ SINGLE_BIT_VALUE_FORMAT_CHARACTER = 'B' """ Format character used to (un)pack singlebit values (values used for writing from and writing to coils or discrete inputs) from structs. .. note:: Its value should not be changed. This attribute exists to be consistend with `MULTI_BIT_VALUE_FORMAT_CHARACTER`. """ MULTI_BIT_VALUE_FORMAT_CHARACTER = 'H' """ Format character used to (un)pack multibit values (values used for writing from and writing to registers) from structs. The format character depends on size of the value and whether values are signed or unsigned. By default multibit values are unsigned and use 16 bits. The default format character used for (un)packing structs is 'H'. .. note:: Its value should not be set directly. Instead use :attr:`SIGNED_VALUES` and :attr:`BIT_SIZE` to modify this value. """ def __init__(self): self.SIGNED_VALUES = os.environ.get('UMODBUS_SIGNED_VALUES', False) self.BIT_SIZE = os.environ.get('UMODBUS_BIT_SIZE', 16) @property def TYPE_CHAR(self): if self.SIGNED_VALUES: return 'h' return 'H' def _set_multi_bit_value_format_character(self): """ Set format character for multibit values. The format character depends on size of the value and whether values are signed or unsigned. """ self.MULTI_BIT_VALUE_FORMAT_CHARACTER = \ self.MULTI_BIT_VALUE_FORMAT_CHARACTER.upper() if self.SIGNED_VALUES: self.MULTI_BIT_VALUE_FORMAT_CHARACTER = \ self.MULTI_BIT_VALUE_FORMAT_CHARACTER.lower() @property def SIGNED_VALUES(self): """ Whether values are signed or not. Default is False. This value can also be set using the environment variable `UMODBUS_SIGNED_VALUES`. """ return self._SIGNED_VALUES @SIGNED_VALUES.setter def SIGNED_VALUES(self, value): """ Set signedness of values. This method effects `Config.MULTI_BIT_VALUE_FORMAT_CHARACTER`. :param value: Boolean indicting if values are signed or not. """ self._SIGNED_VALUES = value self._set_multi_bit_value_format_character() @property def BIT_SIZE(self): """ Bit size of values. Default is 16. This value can also be set using the environment variable `UMODBUS_BIT_SIZE`. """ return self._BIT_SIZE @BIT_SIZE.setter def BIT_SIZE(self, value): """ Set bit size of values. This method effects `Config.MULTI_BIT_VALUE_FORMAT_CHARACTER`. :param value: Number indication bit size. """ self._BIT_SIZE = value self._set_multi_bit_value_format_character() AdvancedClimateSystems-uModbus-4fd98ba/umodbus/exceptions.py000066400000000000000000000051221372201247100244350ustar00rootroot00000000000000class ModbusError(Exception): """ Base class for all Modbus related exception. """ pass class IllegalFunctionError(ModbusError): """ The function code received in the request is not an allowable action for the server. """ error_code = 1 def __str__(self): return 'Function code is not an allowable action for the server.' class IllegalDataAddressError(ModbusError): """ The data address received in the request is not an allowable address for the server. """ error_code = 2 def __str__(self): return self.__doc__ class IllegalDataValueError(ModbusError): """ The value contained in the request data field is not an allowable value for the server. """ error_code = 3 def __str__(self): return self.__doc__ class ServerDeviceFailureError(ModbusError): """ An unrecoverable error occurred. """ error_code = 4 def __str__(self): return 'An unrecoverable error occurred.' class AcknowledgeError(ModbusError): """ The server has accepted the requests and it processing it, but a long duration of time will be required to do so. """ error_code = 5 def __str__(self): return self.__doc__ class ServerDeviceBusyError(ModbusError): """ The server is engaged in a long-duration program command. """ error_code = 6 def __str__(self): return self.__doc__ class MemoryParityError(ModbusError): """ The server attempted to read record file, but detected a parity error in memory. """ error_code = 8 def __repr__(self): return self.__doc__ class GatewayPathUnavailableError(ModbusError): """ The gateway is probably misconfigured or overloaded. """ error_code = 10 def __repr__(self): return self.__doc__ class GatewayTargetDeviceFailedToRespondError(ModbusError): """ Didn't get a response from target device. """ error_code = 11 def __repr__(self): return self.__doc__ error_code_to_exception_map = { IllegalFunctionError.error_code: IllegalFunctionError, IllegalDataAddressError.error_code: IllegalDataAddressError, IllegalDataValueError.error_code: IllegalDataValueError, ServerDeviceFailureError.error_code: ServerDeviceFailureError, AcknowledgeError.error_code: AcknowledgeError, ServerDeviceBusyError.error_code: ServerDeviceBusyError, MemoryParityError.error_code: MemoryParityError, GatewayPathUnavailableError.error_code: GatewayPathUnavailableError, GatewayTargetDeviceFailedToRespondError.error_code: GatewayTargetDeviceFailedToRespondError } AdvancedClimateSystems-uModbus-4fd98ba/umodbus/functions.py000066400000000000000000001502161372201247100242710ustar00rootroot00000000000000""" .. note:: This section is based on `MODBUS Application Protocol Specification V1.1b3`_ The Protocol Data Unit (PDU) is the request or response message and is indepedent of the underlying communication layer. This module only implements requests PDU's. A request PDU contains two parts: a function code and request data. A response PDU contains the function code from the request and response data. The general structure is listed in table below: +---------------+-----------------+ | **Field** | **Size** (bytes)| +---------------+-----------------+ | Function code | 1 | +---------------+-----------------+ | data | N | +---------------+-----------------+ Below you see the request PDU with function code 1, requesting status of 3 coils, starting from coil 100. .. Note: the backslash in the bytes below are escaped using an extra back slash. Without escaping the bytes aren't printed correctly in the HTML output of this docs. To work with the bytes in Python you need to remove the escape sequences. `b'\\x01\\x00d` -> `b\x01\x00d` .. code-block:: python >>> req_pdu = b'\\x01\x00d\\x00\\x03' >>> function_code = req_pdu[:1] >>> function_code b'\\x01' >>> starting_address = req_pdu[1:3] >>> starting_address b'\\x00d' >>> quantity = req_pdu[3:] >>> quantity b'\\x00\\x03' A response PDU could look like this:: >>> resp_pdu = b'\\x01\\x01\\x06' >>> function_code = resp_pdu[:1] >>> function_code b'\\x01' >>> byte_count = resp[1:2] >>> byte_count b'\\x01' >>> coil_status = resp[2:] 'b\\x06' """ from __future__ import division import struct import math try: from inspect import getfullargspec except ImportError: # inspect.getfullargspec was introduced in Python 3.4. # Earlier versions have inspect.getargspec. from inspect import getargspec as getfullargspec try: from functools import reduce except ImportError: pass from umodbus import conf, log from umodbus.exceptions import (error_code_to_exception_map, IllegalDataValueError, IllegalFunctionError, IllegalDataAddressError) from umodbus.utils import memoize, get_function_code_from_request_pdu # Function related to data access. READ_COILS = 1 READ_DISCRETE_INPUTS = 2 READ_HOLDING_REGISTERS = 3 READ_INPUT_REGISTERS = 4 WRITE_SINGLE_COIL = 5 WRITE_SINGLE_REGISTER = 6 WRITE_MULTIPLE_COILS = 15 WRITE_MULTIPLE_REGISTERS = 16 READ_FILE_RECORD = 20 WRITE_FILE_RECORD = 21 READ_WRITE_MULTIPLE_REGISTERS = 23 READ_FIFO_QUEUE = 24 # Diagnostic functions, only available when using serial line. READ_EXCEPTION_STATUS = 7 DIAGNOSTICS = 8 GET_COMM_EVENT_COUNTER = 11 GET_COM_EVENT_LOG = 12 REPORT_SERVER_ID = 17 def pdu_to_function_code_or_raise_error(resp_pdu): """ Parse response PDU and return of :class:`ModbusFunction` or raise error. :param resp_pdu: PDU of response. :return: Subclass of :class:`ModbusFunction` matching the response. :raises ModbusError: When response contains error code. """ function_code = struct.unpack('>B', resp_pdu[0:1])[0] if function_code not in function_code_to_function_map.keys(): error_code = struct.unpack('>B', resp_pdu[1:2])[0] raise error_code_to_exception_map[error_code] return function_code def create_function_from_response_pdu(resp_pdu, req_pdu=None): """ Parse response PDU and return instance of :class:`ModbusFunction` or raise error. :param resp_pdu: PDU of response. :param req_pdu: Request PDU, some functions require more info than in response PDU in order to create instance. Default is None. :return: Number or list with response data. """ function_code = pdu_to_function_code_or_raise_error(resp_pdu) function = function_code_to_function_map[function_code] if req_pdu is not None and \ 'req_pdu' in getfullargspec(function.create_from_response_pdu).args: # NOQA return function.create_from_response_pdu(resp_pdu, req_pdu) return function.create_from_response_pdu(resp_pdu) @memoize def create_function_from_request_pdu(pdu): """ Return function instance, based on request PDU. :param pdu: Array of bytes. :return: Instance of a function. """ function_code = get_function_code_from_request_pdu(pdu) try: function_class = function_code_to_function_map[function_code] except KeyError: raise IllegalFunctionError(function_code) return function_class.create_from_request_pdu(pdu) def expected_response_pdu_size_from_request_pdu(pdu): """ Return number of bytes expected for response PDU, based on request PDU. :param pdu: Array of bytes. :return: number of bytes. """ return create_function_from_request_pdu(pdu).expected_response_pdu_size class ModbusFunction(object): function_code = None class ReadCoils(ModbusFunction): """ Implement Modbus function code 01. "This function code is used to read from 1 to 2000 contiguous status of coils in a remote device. The Request PDU specifies the starting address, i.e. the address of the first coil specified, and the number of coils. In the PDU Coils are addressed starting at zero. Therefore coils numbered 1-16 are addressed as 0-15. The coils in the response message are packed as one coil per bit of the data field. Status is indicated as 1= ON and 0= OFF. The LSB of the first data byte contains the output addressed in the query. The other coils follow toward the high order end of this byte, and from low order to high order in subsequent bytes. If the returned output quantity is not a multiple of eight, the remaining bits in the final data byte will be padded with zeros (toward the high order end of the byte). The Byte Count field specifies the quantity of complete bytes of data." -- MODBUS Application Protocol Specification V1.1b3, chapter 6.1 The request PDU with function code 01 must be 5 bytes: ================ =============== Field Length (bytes) ================ =============== Function code 1 Starting address 2 Quantity 2 ================ =============== The PDU can unpacked to this: .. Note: the backslash in the bytes below are escaped using an extra back slash. Without escaping the bytes aren't printed correctly in the HTML output of this docs. To work with the bytes in Python you need to remove the escape sequences. `b'\\x01\\x00d` -> `b\x01\x00d` .. code-block:: python >>> struct.unpack('>BHH', b'\\x01\\x00d\\x00\\x03') (1, 100, 3) The reponse PDU varies in length, depending on the request. Each 8 coils require 1 byte. The amount of bytes needed represent status of the coils to can be calculated with: bytes = ceil(quantity / 8). This response contains ceil(3 / 8) = 1 byte to describe the status of the coils. The structure of a compleet response PDU looks like this: ================ =============== Field Length (bytes) ================ =============== Function code 1 Byte count 1 Coil status n ================ =============== Assume the status of 102 is 0, 101 is 1 and 100 is also 1. This is binary 011 which is decimal 3. The PDU can packed like this:: >>> struct.pack('>BBB', function_code, byte_count, 3) b'\\x01\\x01\\x03' """ function_code = READ_COILS max_quantity = 2000 format_character = 'B' data = None starting_address = None _quantity = None @property def quantity(self): return self._quantity @quantity.setter def quantity(self, value): """ Set number of coils to read. Quantity must be between 1 and 2000. :param value: Quantity. :raises: IllegalDataValueError. """ if not (1 <= value <= 2000): raise IllegalDataValueError('Quantify field of request must be a ' 'value between 0 and ' '{0}.'.format(2000)) self._quantity = value @property def request_pdu(self): """ Build request PDU to read coils. :return: Byte array of 5 bytes with PDU. """ if None in [self.starting_address, self.quantity]: # TODO Raise proper exception. raise Exception return struct.pack('>BHH', self.function_code, self.starting_address, self.quantity) @classmethod def create_from_request_pdu(cls, pdu): """ Create instance from request PDU. :param pdu: A request PDU. :return: Instance of this class. """ _, starting_address, quantity = struct.unpack('>BHH', pdu) instance = cls() instance.starting_address = starting_address instance.quantity = quantity return instance @property def expected_response_pdu_size(self): """ Return number of bytes expected for response PDU. :return: number of bytes. """ return 2 + int(math.ceil(self.quantity / 8)) def create_response_pdu(self, data): """ Create response pdu. :param data: A list with 0's and/or 1's. :return: Byte array of at least 3 bytes. """ log.debug('Create single bit response pdu {0}.'.format(data)) bytes_ = [data[i:i + 8] for i in range(0, len(data), 8)] # Reduce each all bits per byte to a number. Byte # [0, 0, 0, 0, 0, 1, 1, 1] is intepreted as binary en is decimal 3. for index, byte in enumerate(bytes_): bytes_[index] = \ reduce(lambda a, b: (a << 1) + b, list(reversed(byte))) log.debug('Reduced single bit data to {0}.'.format(bytes_)) # The first 2 B's of the format encode the function code (1 byte) and # the length (1 byte) of the following byte series. Followed by # a B # for every byte in the series of bytes. 3 lead to the format '>BBB' # and 257 lead to the format '>BBBB'. fmt = '>BB' + self.format_character * len(bytes_) return struct.pack(fmt, self.function_code, len(bytes_), *bytes_) @classmethod def create_from_response_pdu(cls, resp_pdu, req_pdu): """ Create instance from response PDU. Response PDU is required together with the quantity of coils read. :param resp_pdu: Byte array with request PDU. :param quantity: Number of coils read. :return: Instance of :class:`ReadCoils`. """ read_coils = cls() read_coils.quantity = struct.unpack('>H', req_pdu[-2:])[0] byte_count = struct.unpack('>B', resp_pdu[1:2])[0] fmt = '>' + ('B' * byte_count) bytes_ = struct.unpack(fmt, resp_pdu[2:]) data = list() for i, value in enumerate(bytes_): padding = 8 if (read_coils.quantity - (8 * i)) // 8 > 0 \ else read_coils.quantity % 8 fmt = '{{0:0{padding}b}}'.format(padding=padding) # Create binary representation of integer, convert it to a list # and reverse the list. data = data + [int(i) for i in fmt.format(value)][::-1] read_coils.data = data return read_coils def execute(self, slave_id, route_map): """ Execute the Modbus function registered for a route. :param slave_id: Slave id. :param route_map: Instance of modbus.route.Map. :return: Result of call to endpoint. """ values = [] for address in range(self.starting_address, self.starting_address + self.quantity): endpoint = route_map.match(slave_id, self.function_code, address) if endpoint is None: raise IllegalDataAddressError() values.append(endpoint(slave_id=slave_id, address=address, function_code=self.function_code)) return values class ReadDiscreteInputs(ModbusFunction): """ Implement Modbus function code 02. "This function code is used to read from 1 to 2000 contiguous status of discrete inputs in a remote device. The Request PDU specifies the starting address, i.e. the address of the first input specified, and the number of inputs. In the PDU Discrete Inputs are addressed starting at zero. Therefore Discrete inputs numbered 1-16 are addressed as 0-15. The discrete inputs in the response message are packed as one input per bit of the data field. Status is indicated as 1= ON; 0= OFF. The LSB of the first data byte contains the input addressed in the query. The other inputs follow toward the high order end of this byte, and from low order to high order in subsequent bytes. If the returned input quantity is not a multiple of eight, the remaining bits in the final d ata byte will be padded with zeros (toward the high order end of the byte). The Byte Count field specifies the quantity of complete bytes of data." -- MODBUS Application Protocol Specification V1.1b3, chapter 6.2 The request PDU with function code 02 must be 5 bytes: ================ =============== Field Length (bytes) ================ =============== Function code 1 Starting address 2 Quantity 2 ================ =============== The PDU can unpacked to this: .. Note: the backslash in the bytes below are escaped using an extra back slash. Without escaping the bytes aren't printed correctly in the HTML output of this docs. To work with the bytes in Python you need to remove the escape sequences. `b'\\x01\\x00d` -> `b\x01\x00d` .. code-block:: python >>> struct.unpack('>BHH', b'\\x02\\x00d\\x00\\x03') (2, 100, 3) The reponse PDU varies in length, depending on the request. 8 inputs require 1 byte. The amount of bytes needed represent status of the inputs to can be calculated with: bytes = ceil(quantity / 8). This response contains ceil(3 / 8) = 1 byte to describe the status of the inputs. The structure of a compleet response PDU looks like this: ================ =============== Field Length (bytes) ================ =============== Function code 1 Byte count 1 Coil status n ================ =============== Assume the status of 102 is 0, 101 is 1 and 100 is also 1. This is binary 011 which is decimal 3. The PDU can packed like this:: >>> struct.pack('>BBB', function_code, byte_count, 3) b'\\x02\\x01\\x03' """ function_code = READ_DISCRETE_INPUTS max_quantity = 2000 format_character = 'B' data = None starting_address = None _quantity = None @property def quantity(self): return self._quantity @quantity.setter def quantity(self, value): """ Set number of inputs to read. Quantity must be between 1 and 2000. :param value: Quantity. :raises: IllegalDataValueError. """ if not (1 <= value <= 2000): raise IllegalDataValueError('Quantify field of request must be a ' 'value between 0 and ' '{0}.'.format(2000)) self._quantity = value @property def request_pdu(self): """ Build request PDU to read discrete inputs. :return: Byte array of 5 bytes with PDU. """ if None in [self.starting_address, self.quantity]: # TODO Raise proper exception. raise Exception return struct.pack('>BHH', self.function_code, self.starting_address, self.quantity) @classmethod def create_from_request_pdu(cls, pdu): """ Create instance from request PDU. :param pdu: A request PDU. :return: Instance of this class. """ _, starting_address, quantity = struct.unpack('>BHH', pdu) instance = cls() instance.starting_address = starting_address instance.quantity = quantity return instance @property def expected_response_pdu_size(self): """ Return number of bytes expected for response PDU. :return: number of bytes. """ return 2 + int(math.ceil(self.quantity / 8)) def create_response_pdu(self, data): """ Create response pdu. :param data: A list with 0's and/or 1's. :return: Byte array of at least 3 bytes. """ log.debug('Create single bit response pdu {0}.'.format(data)) bytes_ = [data[i:i + 8] for i in range(0, len(data), 8)] # Reduce each all bits per byte to a number. Byte # [0, 0, 0, 0, 0, 1, 1, 1] is intepreted as binary en is decimal 3. for index, byte in enumerate(bytes_): bytes_[index] = \ reduce(lambda a, b: (a << 1) + b, list(reversed(byte))) log.debug('Reduced single bit data to {0}.'.format(bytes_)) # The first 2 B's of the format encode the function code (1 byte) and # the length (1 byte) of the following byte series. Followed by # a B # for every byte in the series of bytes. 3 lead to the format '>BBB' # and 257 lead to the format '>BBBB'. fmt = '>BB' + self.format_character * len(bytes_) return struct.pack(fmt, self.function_code, len(bytes_), *bytes_) @classmethod def create_from_response_pdu(cls, resp_pdu, req_pdu): """ Create instance from response PDU. Response PDU is required together with the quantity of inputs read. :param resp_pdu: Byte array with request PDU. :param quantity: Number of inputs read. :return: Instance of :class:`ReadDiscreteInputs`. """ read_discrete_inputs = cls() read_discrete_inputs.quantity = struct.unpack('>H', req_pdu[-2:])[0] byte_count = struct.unpack('>B', resp_pdu[1:2])[0] fmt = '>' + ('B' * byte_count) bytes_ = struct.unpack(fmt, resp_pdu[2:]) data = list() for i, value in enumerate(bytes_): padding = 8 if (read_discrete_inputs.quantity - (8 * i)) // 8 > 0 \ else read_discrete_inputs.quantity % 8 fmt = '{{0:0{padding}b}}'.format(padding=padding) # Create binary representation of integer, convert it to a list # and reverse the list. data = data + [int(i) for i in fmt.format(value)][::-1] read_discrete_inputs.data = data return read_discrete_inputs def execute(self, slave_id, route_map): """ Execute the Modbus function registered for a route. :param slave_id: Slave id. :param route_map: Instance of modbus.route.Map. :return: Result of call to endpoint. """ values = [] for address in range(self.starting_address, self.starting_address + self.quantity): endpoint = route_map.match(slave_id, self.function_code, address) if endpoint is None: raise IllegalDataAddressError() values.append(endpoint(slave_id=slave_id, address=address, function_code=self.function_code)) return values class ReadHoldingRegisters(ModbusFunction): """ Implement Modbus function code 03. "This function code is used to read the contents of a contiguous block of holding registers in a remote device. The Request PDU specifies the starting register address and the number of registers. In the PDU Registers are addressed starting at zero. Therefore registers numbered 1-16 are addressed as 0-15. The register data in the response message are packed as two bytes per register, with the binary contents right justified within each byte. For each register, the first byte contains the high order bits and the second contains the low order bits." -- MODBUS Application Protocol Specification V1.1b3, chapter 6.3 The request PDU with function code 03 must be 5 bytes: ================ =============== Field Length (bytes) ================ =============== Function code 1 Starting address 2 Quantity 2 ================ =============== The PDU can unpacked to this: .. Note: the backslash in the bytes below are escaped using an extra back slash. Without escaping the bytes aren't printed correctly in the HTML output of this docs. To work with the bytes in Python you need to remove the escape sequences. `b'\\x01\\x00d` -> `b\x01\x00d` .. code-block:: python >>> struct.unpack('>BHH', b'\\x03\\x00d\\x00\\x03') (3, 100, 3) The reponse PDU varies in length, depending on the request. By default, holding registers are 16 bit (2 bytes) values. So values of 3 holding registers is expressed in 2 * 3 = 6 bytes. ================ =============== Field Length (bytes) ================ =============== Function code 1 Byte count 1 Register values Quantity * 2 ================ =============== Assume the value of 100 is 8, 101 is 0 and 102 is also 15. The PDU can packed like this:: >>> data = [8, 0, 15] >>> struct.pack('>BBHHH', function_code, len(data) * 2, *data) b'\\x03\\x06\\x00\\x08\\x00\\x00\\x00\\x0f' """ function_code = READ_HOLDING_REGISTERS max_quantity = 0x007D data = None starting_address = None _quantity = None @property def quantity(self): return self._quantity @quantity.setter def quantity(self, value): """ Set number of registers to read. Quantity must be between 1 and 0x00FD. :param value: Quantity. :raises: IllegalDataValueError. """ if not (1 <= value <= 0x007D): raise IllegalDataValueError('Quantify field of request must be a ' 'value between 0 and ' '{0}.'.format(0x007D)) self._quantity = value @property def request_pdu(self): """ Build request PDU to read coils. :return: Byte array of 5 bytes with PDU. """ if None in [self.starting_address, self.quantity]: # TODO Raise proper exception. raise Exception return struct.pack('>BHH', self.function_code, self.starting_address, self.quantity) @classmethod def create_from_request_pdu(cls, pdu): """ Create instance from request PDU. :param pdu: A request PDU. :return: Instance of this class. """ _, starting_address, quantity = struct.unpack('>BHH', pdu) instance = cls() instance.starting_address = starting_address instance.quantity = quantity return instance @property def expected_response_pdu_size(self): """ Return number of bytes expected for response PDU. :return: number of bytes. """ return 2 + self.quantity * 2 def create_response_pdu(self, data): """ Create response pdu. :param data: A list with values. :return: Byte array of at least 4 bytes. """ log.debug('Create multi bit response pdu {0}.'.format(data)) fmt = '>BB' + conf.TYPE_CHAR * len(data) return struct.pack(fmt, self.function_code, len(data) * 2, *data) @classmethod def create_from_response_pdu(cls, resp_pdu, req_pdu): """ Create instance from response PDU. Response PDU is required together with the number of registers read. :param resp_pdu: Byte array with request PDU. :param quantity: Number of coils read. :return: Instance of :class:`ReadCoils`. """ read_holding_registers = cls() read_holding_registers.quantity = struct.unpack('>H', req_pdu[-2:])[0] read_holding_registers.byte_count = \ struct.unpack('>B', resp_pdu[1:2])[0] fmt = '>' + (conf.TYPE_CHAR * read_holding_registers.quantity) read_holding_registers.data = list(struct.unpack(fmt, resp_pdu[2:])) return read_holding_registers def execute(self, slave_id, route_map): """ Execute the Modbus function registered for a route. :param slave_id: Slave id. :param route_map: Instance of modbus.route.Map. :return: Result of call to endpoint. """ values = [] for address in range(self.starting_address, self.starting_address + self.quantity): endpoint = route_map.match(slave_id, self.function_code, address) if endpoint is None: raise IllegalDataAddressError() values.append(endpoint(slave_id=slave_id, address=address, function_code=self.function_code)) return values class ReadInputRegisters(ModbusFunction): """ Implement Modbus function code 04. "This function code is used to read from 1 to 125 contiguous input registers in a remote device. The Request PDU specifies the starting register address and the number of registers. In the PDU Registers are addressed starting at zero. Therefore input registers numbered 1-16 are addressed as 0-15. The register data in the response message are packed as two bytes per register, with the binary contents right justified within each byte. For each register, the first byte contains the high order bits and the second contains the low order bits." -- MODBUS Application Protocol Specification V1.1b3, chapter 6.4 The request PDU with function code 04 must be 5 bytes: ================ =============== Field Length (bytes) ================ =============== Function code 1 Starting address 2 Quantity 2 ================ =============== The PDU can unpacked to this: .. Note: the backslash in the bytes below are escaped using an extra back slash. Without escaping the bytes aren't printed correctly in the HTML output of this docs. To work with the bytes in Python you need to remove the escape sequences. `b'\\x01\\x00d` -> `b\x01\x00d` .. code-block:: python >>> struct.unpack('>BHH', b'\\x04\\x00d\\x00\\x03') (4, 100, 3) The reponse PDU varies in length, depending on the request. By default, holding registers are 16 bit (2 bytes) values. So values of 3 holding registers is expressed in 2 * 3 = 6 bytes. ================ =============== Field Length (bytes) ================ =============== Function code 1 Byte count 1 Register values Quantity * 2 ================ =============== Assume the value of 100 is 8, 101 is 0 and 102 is also 15. The PDU can packed like this:: >>> data = [8, 0, 15] >>> struct.pack('>BBHHH', function_code, len(data) * 2, *data) b'\\x04\\x06\\x00\\x08\\x00\\x00\\x00\\x0f' """ function_code = READ_INPUT_REGISTERS max_quantity = 0x007D data = None starting_address = None _quantity = None @property def quantity(self): return self._quantity @quantity.setter def quantity(self, value): """ Set number of registers to read. Quantity must be between 1 and 0x00FD. :param value: Quantity. :raises: IllegalDataValueError. """ if not (1 <= value <= 0x007D): raise IllegalDataValueError('Quantify field of request must be a ' 'value between 0 and ' '{0}.'.format(0x007D)) self._quantity = value @property def request_pdu(self): """ Build request PDU to read coils. :return: Byte array of 5 bytes with PDU. """ if None in [self.starting_address, self.quantity]: # TODO Raise proper exception. raise Exception return struct.pack('>BHH', self.function_code, self.starting_address, self.quantity) @classmethod def create_from_request_pdu(cls, pdu): """ Create instance from request PDU. :param pdu: A request PDU. :return: Instance of this class. """ _, starting_address, quantity = struct.unpack('>BHH', pdu) instance = cls() instance.starting_address = starting_address instance.quantity = quantity return instance @property def expected_response_pdu_size(self): """ Return number of bytes expected for response PDU. :return: number of bytes. """ return 2 + self.quantity * 2 def create_response_pdu(self, data): """ Create response pdu. :param data: A list with values. :return: Byte array of at least 4 bytes. """ log.debug('Create multi bit response pdu {0}.'.format(data)) fmt = '>BB' + conf.TYPE_CHAR * len(data) return struct.pack(fmt, self.function_code, len(data) * 2, *data) @classmethod def create_from_response_pdu(cls, resp_pdu, req_pdu): """ Create instance from response PDU. Response PDU is required together with the number of registers read. :param resp_pdu: Byte array with request PDU. :param quantity: Number of coils read. :return: Instance of :class:`ReadCoils`. """ read_input_registers = cls() read_input_registers.quantity = struct.unpack('>H', req_pdu[-2:])[0] fmt = '>' + (conf.TYPE_CHAR * read_input_registers.quantity) read_input_registers.data = list(struct.unpack(fmt, resp_pdu[2:])) return read_input_registers def execute(self, slave_id, route_map): """ Execute the Modbus function registered for a route. :param slave_id: Slave id. :param route_map: Instance of modbus.route.Map. :return: Result of call to endpoint. """ values = [] for address in range(self.starting_address, self.starting_address + self.quantity): endpoint = route_map.match(slave_id, self.function_code, address) if endpoint is None: raise IllegalDataAddressError() values.append(endpoint(slave_id=slave_id, address=address, function_code=self.function_code)) return values class WriteSingleCoil(ModbusFunction): """ Implement Modbus function code 05. "This function code is used to write a single output to either ON or OFF in a remote device. The requested ON/OFF state is specified by a constant in the request data field. A value of FF 00 hex requests the output to be ON. A value of 00 00 requests it to be OFF. All other values are illegal and will not affect the output. The Request PDU specifies the address of the coil to be forced. Coils are addressed starting at zero. Therefore coil numbered 1 is addressed as 0. The requested ON/OFF state is specified by a constant in the Coil Value field. A value of 0XFF00 requests the coil to be ON. A value of 0X0000 requests the coil to be off. All other values are illegal and will not affect the coil. The normal response is an echo of the request, returned after the coil state has been written." -- MODBUS Application Protocol Specification V1.1b3, chapter 6.5 The request PDU with function code 05 must be 5 bytes: ================ =============== Field Length (bytes) ================ =============== Function code 1 Address 2 Value 2 ================ =============== The PDU can unpacked to this: .. Note: the backslash in the bytes below are escaped using an extra back slash. Without escaping the bytes aren't printed correctly in the HTML output of this docs. To work with the bytes in Python you need to remove the escape sequences. `b'\\x01\\x00d` -> `b\x01\x00d` .. code-block:: python >>> struct.unpack('>BHH', b'\\x05\\x00d\\xFF\\x00') (5, 100, 65280) The reponse PDU is a copy of the request PDU. ================ =============== Field Length (bytes) ================ =============== Function code 1 Address 2 Value 2 ================ =============== """ function_code = WRITE_SINGLE_COIL format_character = 'B' address = None data = None _value = None @property def value(self): if self._value == 0xFF00: return 1 return self._value @value.setter def value(self, value): if value not in [0, 1, 0xFF00]: raise IllegalDataValueError value = 0xFF00 if value == 1 else value self._value = value @property def request_pdu(self): """ Build request PDU to write single coil. :return: Byte array of 5 bytes with PDU. """ if None in [self.address, self.value]: # TODO Raise proper exception. raise Exception return struct.pack('>BHH', self.function_code, self.address, self._value) @classmethod def create_from_request_pdu(cls, pdu): """ Create instance from request PDU. :param pdu: A response PDU. :return: Instance of this class. """ _, address, value = struct.unpack('>BHH', pdu) value = 1 if value == 0xFF00 else value instance = cls() instance.address = address instance.value = value return instance @property def expected_response_pdu_size(self): """ Return number of bytes expected for response PDU. :return: number of bytes. """ return 5 def create_response_pdu(self): """ Create response pdu. :param data: A list with values. :return: Byte array of at least 4 bytes. """ fmt = '>BHH' return struct.pack(fmt, self.function_code, self.address, self._value) @classmethod def create_from_response_pdu(cls, resp_pdu): """ Create instance from response PDU. :param resp_pdu: Byte array with request PDU. :return: Instance of :class:`WriteSingleCoil`. """ write_single_coil = cls() address, value = struct.unpack('>HH', resp_pdu[1:5]) value = 1 if value == 0xFF00 else value write_single_coil.address = address write_single_coil.data = value return write_single_coil def execute(self, slave_id, route_map): """ Execute the Modbus function registered for a route. :param slave_id: Slave id. :param route_map: Instance of modbus.route.Map. """ endpoint = route_map.match(slave_id, self.function_code, self.address) if endpoint is None: raise IllegalDataAddressError() endpoint(slave_id=slave_id, address=self.address, value=self.value, function_code=self.function_code) class WriteSingleRegister(ModbusFunction): """ Implement Modbus function code 06. "This function code is used to write a single holding register in a remote device. The Request PDU specifies the address of the register to be written. Registers are addressed starting at zero. Therefore register numbered 1 is addressed as 0. The normal response is an echo of the request, returned after the register contents have been written." -- MODBUS Application Protocol Specification V1.1b3, chapter 6.6 The request PDU with function code 06 must be 5 bytes: ================ =============== Field Length (bytes) ================ =============== Function code 1 Address 2 Value 2 ================ =============== The PDU can unpacked to this: .. Note: the backslash in the bytes below are escaped using an extra back slash. Without escaping the bytes aren't printed correctly in the HTML output of this docs. To work with the bytes in Python you need to remove the escape sequences. `b'\\x01\\x00d` -> `b\x01\x00d` .. code-block:: python >>> struct.unpack('>BHH', b'\\x06\\x00d\\x00\\x03') (6, 100, 3) The reponse PDU is a copy of the request PDU. ================ =============== Field Length (bytes) ================ =============== Function code 1 Address 2 Value 2 ================ =============== """ function_code = WRITE_SINGLE_REGISTER address = None data = None _value = None @property def value(self): return self._value @value.setter def value(self, value): """ Value to be written on register. :param value: An integer. :raises: IllegalDataValueError when value isn't in range. """ try: struct.pack('>' + conf.TYPE_CHAR, value) except struct.error: raise IllegalDataValueError self._value = value @property def request_pdu(self): """ Build request PDU to write single register. :return: Byte array of 5 bytes with PDU. """ if None in [self.address, self.value]: # TODO Raise proper exception. raise Exception return struct.pack('>BH' + conf.TYPE_CHAR, self.function_code, self.address, self.value) @classmethod def create_from_request_pdu(cls, pdu): """ Create instance from request PDU. :param pdu: A request PDU. :return: Instance of this class. """ _, address, value = \ struct.unpack('>BH' + conf.MULTI_BIT_VALUE_FORMAT_CHARACTER, pdu) instance = cls() instance.address = address instance.value = value return instance @property def expected_response_pdu_size(self): """ Return number of bytes expected for response PDU. :return: number of bytes. """ return 5 def create_response_pdu(self): fmt = '>BH' + conf.TYPE_CHAR return struct.pack(fmt, self.function_code, self.address, self.value) @classmethod def create_from_response_pdu(cls, resp_pdu): """ Create instance from response PDU. :param resp_pdu: Byte array with request PDU. :return: Instance of :class:`WriteSingleRegister`. """ write_single_register = cls() address, value = struct.unpack('>H' + conf.TYPE_CHAR, resp_pdu[1:5]) write_single_register.address = address write_single_register.data = value return write_single_register def execute(self, slave_id, route_map): """ Execute the Modbus function registered for a route. :param slave_id: Slave id. :param route_map: Instance of modbus.route.Map. """ endpoint = route_map.match(slave_id, self.function_code, self.address) if endpoint is None: raise IllegalDataAddressError() endpoint(slave_id=slave_id, address=self.address, value=self.value, function_code=self.function_code) class WriteMultipleCoils(ModbusFunction): """ Implement Modbus function 15 (0x0F) Write Multiple Coils. "This function code is used to force each coil in a sequence of coils to either ON or OFF in a remote device. The Request PDU specifies the coil references to be forced. Coils are addressed starting at zero. Therefore coil numbered 1 is addressed as 0. The requested ON/OFF states are specified by contents of the request data field. A logical '1' in a bit position of the field requests the corresponding output to be ON. A logical '0' requests it to be OFF. The normal response returns the function code, starting address, and quantity of coils forced." -- MODBUS Application Protocol Specification V1.1b3, chapter 6.11 The request PDU with function code 15 must be at least 7 bytes: ================ =============== Field Length (bytes) ================ =============== Function code 1 Starting Address 2 Quantity 2 Byte count 1 Value n ================ =============== The PDU can unpacked to this: .. Note: the backslash in the bytes below are escaped using an extra back slash. Without escaping the bytes aren't printed correctly in the HTML output of this docs. To work with the bytes in Python you need to remove the escape sequences. `b'\\x01\\x00d` -> `b\x01\x00d` .. code-block:: python >>> struct.unpack('>BHHBB', b'\\x0f\\x00d\\x00\\x03\\x01\\x05') (16, 100, 3, 1, 5) The reponse PDU is 5 bytes and contains following structure: ================ =============== Field Length (bytes) ================ =============== Function code 1 Starting address 2 Quantity 2 ================ =============== """ function_code = WRITE_MULTIPLE_COILS starting_address = None _values = None _data = None @property def values(self): return self._values @values.setter def values(self, values): if not (1 <= len(values) <= 0x7B0): raise IllegalDataValueError for value in values: if value not in [0, 1]: raise IllegalDataValueError self._values = values @property def request_pdu(self): if None in [self.starting_address, self._values]: raise IllegalDataValueError bytes_ = [self.values[i:i + 8] for i in range(0, len(self.values), 8)] # Reduce each all bits per byte to a number. Byte # [0, 0, 0, 0, 0, 1, 1, 1] is intepreted as binary en is decimal 3. for index, byte in enumerate(bytes_): bytes_[index] = \ reduce(lambda a, b: (a << 1) + b, list(reversed(byte))) fmt = '>BHHB' + 'B' * len(bytes_) return struct.pack(fmt, self.function_code, self.starting_address, len(self.values), (len(self.values) + 7) // 8, *bytes_) @classmethod def create_from_request_pdu(cls, pdu): """ Create instance from request PDU. This method requires some clarification regarding the unpacking of the status that are being passed to the callbacks. A coil status can be 0 or 1. The request PDU contains at least 1 byte, representing the status for 1 to 8 coils. Assume a request with starting address 100, quantity set to 3 and the value byte is 6. 0b110 is the binary reprensention of decimal 6. The Least Significant Bit (LSB) is status of coil with starting address. So status of coil 100 is 0, status of coil 101 is 1 and status of coil 102 is 1 too. coil address 102 101 100 1 1 0 Again, assume starting address 100 and byte value is 6. But now quantity is 4. So the value byte is addressing 4 coils. The binary representation of 6 is now 0b0110. LSB again is 0, meaning status of coil 100 is 0. Status of 101 and 102 is 1, like in the previous example. Status of coil 104 is 0. coil address 104 102 101 100 0 1 1 0 In short: the binary representation of the byte value is in reverse mapped to the coil addresses. In table below you can see some more examples. # quantity value binary representation | 102 101 100 == ======== ===== ===================== | === === === 01 1 0 0b0 - - 0 02 1 1 0b1 - - 1 03 2 0 0b00 - 0 0 04 2 1 0b01 - 0 1 05 2 2 0b10 - 1 0 06 2 3 0b11 - 1 1 07 3 0 0b000 0 0 0 08 3 1 0b001 0 0 1 09 3 2 0b010 0 1 0 10 3 3 0b011 0 1 1 11 3 4 0b100 1 0 0 12 3 5 0b101 1 0 1 13 3 6 0b110 1 1 0 14 3 7 0b111 1 1 1 :param pdu: A request PDU. """ _, starting_address, quantity, byte_count = \ struct.unpack('>BHHB', pdu[:6]) fmt = '>' + (conf.SINGLE_BIT_VALUE_FORMAT_CHARACTER * byte_count) values = struct.unpack(fmt, pdu[6:]) res = list() for i, value in enumerate(values): padding = 8 if (quantity - (8 * i)) // 8 > 0 else quantity % 8 fmt = '{{0:0{padding}b}}'.format(padding=padding) # Create binary representation of integer, convert it to a list # and reverse the list. res = res + [int(i) for i in fmt.format(value)][::-1] instance = cls() instance.starting_address = starting_address instance.quantity = quantity instance.values = res return instance @property def expected_response_pdu_size(self): """ Return number of bytes expected for response PDU. :return: number of bytes. """ return 5 def create_response_pdu(self): """ Create response pdu. :param data: A list with values. :return: Byte array 5 bytes. """ return struct.pack('>BHH', self.function_code, self.starting_address, len(self.values)) @classmethod def create_from_response_pdu(cls, resp_pdu): write_multiple_coils = cls() starting_address, data = struct.unpack('>HH', resp_pdu[1:5]) write_multiple_coils.starting_address = starting_address write_multiple_coils.data = data return write_multiple_coils def execute(self, slave_id, route_map): """ Execute the Modbus function registered for a route. :param slave_id: Slave id. :param route_map: Instance of modbus.route.Map. """ for index, value in enumerate(self.values): address = self.starting_address + index endpoint = route_map.match(slave_id, self.function_code, address) if endpoint is None: raise IllegalDataAddressError() endpoint(slave_id=slave_id, address=address, value=value, function_code=self.function_code) class WriteMultipleRegisters(ModbusFunction): """ Implement Modbus function 16 (0x10) Write Multiple Registers. "This function code is used to write a block of contiguous registers (1 to 123 registers) in a remote device. The requested written values are specified in the request data field. Data is packed as two bytes per register. The normal response returns the function code, starting address, and quantity of registers written." -- MODBUS Application Protocol Specification V1.1b3, chapter 6.12 The request PDU with function code 16 must be at least 8 bytes: ================ =============== Field Length (bytes) ================ =============== Function code 1 Starting Address 2 Quantity 2 Byte count 1 Value Quantity * 2 ================ =============== The PDU can unpacked to this: .. Note: the backslash in the bytes below are escaped using an extra back slash. Without escaping the bytes aren't printed correctly in the HTML output of this docs. To work with the bytes in Python you need to remove the escape sequences. `b'\\x01\\x00d` -> `b\x01\x00d` .. code-block:: python >>> struct.unpack('>BHHBH', b'\\x10\\x00d\\x00\\x01\\x02\\x00\\x05') (16, 100, 1, 2, 5) The reponse PDU is 5 bytes and contains following structure: ================ =============== Field Length (bytes) ================ =============== Function code 1 Starting address 2 Quantity 2 ================ =============== """ function_code = WRITE_MULTIPLE_REGISTERS starting_address = None _values = None _data = None @property def values(self): return self._values @values.setter def values(self, values): if not (1 <= len(values) <= 0x7B0): raise IllegalDataValueError for value in values: try: struct.pack(">" + conf.MULTI_BIT_VALUE_FORMAT_CHARACTER, value) except struct.error: raise IllegalDataValueError self._values = values self._values = values @property def request_pdu(self): fmt = '>BHHB' + (conf.TYPE_CHAR * len(self.values)) return struct.pack(fmt, self.function_code, self.starting_address, len(self.values), len(self.values) * 2, *self.values) @classmethod def create_from_request_pdu(cls, pdu): """ Create instance from request PDU. :param pdu: A request PDU. :return: Instance of this class. """ _, starting_address, quantity, byte_count = \ struct.unpack('>BHHB', pdu[:6]) # Values are 16 bit, so each value takes up 2 bytes. fmt = '>' + (conf.MULTI_BIT_VALUE_FORMAT_CHARACTER * int((byte_count / 2))) values = list(struct.unpack(fmt, pdu[6:])) instance = cls() instance.starting_address = starting_address instance.values = values return instance @property def expected_response_pdu_size(self): """ Return number of bytes expected for response PDU. :return: number of bytes. """ return 5 def create_response_pdu(self): """ Create response pdu. :param data: A list with values. :return: Byte array 5 bytes. """ return struct.pack('>BHH', self.function_code, self.starting_address, len(self.values)) @classmethod def create_from_response_pdu(cls, resp_pdu): write_multiple_registers = cls() starting_address, data = struct.unpack('>HH', resp_pdu[1:5]) write_multiple_registers.starting_address = starting_address write_multiple_registers.data = data return write_multiple_registers def execute(self, slave_id, route_map): """ Execute the Modbus function registered for a route. :param slave_id: Slave id. :param route_map: Instance of modbus.route.Map. """ for index, value in enumerate(self.values): address = self.starting_address + index endpoint = route_map.match(slave_id, self.function_code, address) if endpoint is None: raise IllegalDataAddressError() endpoint(slave_id=slave_id, address=address, value=value, function_code=self.function_code) function_code_to_function_map = { READ_COILS: ReadCoils, READ_DISCRETE_INPUTS: ReadDiscreteInputs, READ_HOLDING_REGISTERS: ReadHoldingRegisters, READ_INPUT_REGISTERS: ReadInputRegisters, WRITE_SINGLE_COIL: WriteSingleCoil, WRITE_SINGLE_REGISTER: WriteSingleRegister, WRITE_MULTIPLE_COILS: WriteMultipleCoils, WRITE_MULTIPLE_REGISTERS: WriteMultipleRegisters, } AdvancedClimateSystems-uModbus-4fd98ba/umodbus/route.py000066400000000000000000000017721372201247100234210ustar00rootroot00000000000000class Map: def __init__(self): self._rules = [] def add_rule(self, endpoint, slave_ids, function_codes, addresses): self._rules.append(DataRule(endpoint, slave_ids, function_codes, addresses)) def match(self, slave_id, function_code, address): for rule in self._rules: if rule.match(slave_id, function_code, address): return rule.endpoint class DataRule: def __init__(self, endpoint, slave_ids, function_codes, addresses): self.endpoint = endpoint self.slave_ids = slave_ids self.function_codes = function_codes self.addresses = addresses def match(self, slave_id, function_code, address): # A constraint of None matches any value matches = lambda values, v: values is None or v in values return matches(self.slave_ids, slave_id) and \ matches(self.function_codes, function_code) and \ matches(self.addresses, address) AdvancedClimateSystems-uModbus-4fd98ba/umodbus/server/000077500000000000000000000000001372201247100232105ustar00rootroot00000000000000AdvancedClimateSystems-uModbus-4fd98ba/umodbus/server/__init__.py000066400000000000000000000100011372201247100253110ustar00rootroot00000000000000try: from socketserver import BaseRequestHandler except ImportError: from SocketServer import BaseRequestHandler from binascii import hexlify from umodbus import log from umodbus.functions import create_function_from_request_pdu from umodbus.exceptions import ModbusError, ServerDeviceFailureError from umodbus.utils import (get_function_code_from_request_pdu, pack_exception_pdu, recv_exactly) def route(self, slave_ids=None, function_codes=None, addresses=None): """ A decorator that is used to register an endpoint for a given rule:: @server.route(slave_ids=[1], function_codes=[1, 2], addresses=list(range(100, 200))) # NOQA def read_single_bit_values(slave_id, address): return random.choise([0, 1]) Any argument can be omitted to match any value. :param slave_ids: A list (or iterable) of slave ids. :param function_codes: A list (or iterable) of function codes. :param addresses: A list (or iterable) of addresses. """ def inner(f): self.route_map.add_rule(f, slave_ids, function_codes, addresses) return f return inner class AbstractRequestHandler(BaseRequestHandler): """ A subclass of :class:`socketserver.BaseRequestHandler` dispatching incoming Modbus requests using the server's :attr:`route_map`. """ def handle(self): try: while True: try: mbap_header = recv_exactly(self.request.recv, 7) remaining = self.get_meta_data(mbap_header)['length'] - 1 request_pdu = recv_exactly(self.request.recv, remaining) except ValueError: return response_adu = self.process(mbap_header + request_pdu) self.respond(response_adu) except: log.exception('Error while handling request') raise def process(self, request_adu): """ Process request ADU and return response. :param request_adu: A bytearray containing the ADU request. :return: A bytearray containing the response of the ADU request. """ meta_data = self.get_meta_data(request_adu) request_pdu = self.get_request_pdu(request_adu) response_pdu = self.execute_route(meta_data, request_pdu) response_adu = self.create_response_adu(meta_data, response_pdu) return response_adu def execute_route(self, meta_data, request_pdu): """ Execute configured route based on requests meta data and request PDU. :param meta_data: A dict with meta data. It must at least contain key 'unit_id'. :param request_pdu: A bytearray containing request PDU. :return: A bytearry containing reponse PDU. """ try: function = create_function_from_request_pdu(request_pdu) results =\ function.execute(meta_data['unit_id'], self.server.route_map) try: # ReadFunction's use results of callbacks to build response # PDU... return function.create_response_pdu(results) except TypeError: # ...other functions don't. return function.create_response_pdu() except ModbusError as e: function_code = get_function_code_from_request_pdu(request_pdu) return pack_exception_pdu(function_code, e.error_code) except Exception: log.exception('Could not handle request') function_code = get_function_code_from_request_pdu(request_pdu) return pack_exception_pdu(function_code, ServerDeviceFailureError.error_code) def respond(self, response_adu): """ Send response ADU back to client. :param response_adu: A bytearray containing the response of an ADU. """ log.debug('--> {0} - {1}.'.format(self.client_address[0], hexlify(response_adu))) self.request.sendall(response_adu) AdvancedClimateSystems-uModbus-4fd98ba/umodbus/server/serial/000077500000000000000000000000001372201247100244675ustar00rootroot00000000000000AdvancedClimateSystems-uModbus-4fd98ba/umodbus/server/serial/__init__.py000066400000000000000000000110641372201247100266020ustar00rootroot00000000000000import struct from binascii import hexlify from types import MethodType from serial import SerialTimeoutException from umodbus import log from umodbus.route import Map from umodbus.server import route from umodbus.functions import create_function_from_request_pdu from umodbus.exceptions import ModbusError, ServerDeviceFailureError from umodbus.utils import (get_function_code_from_request_pdu, pack_exception_pdu) from umodbus.client.serial.redundancy_check import CRCError def get_server(server_class, serial_port): """ Return instance of :param:`server_class` with :param:`request_handler` bound to it. This method also binds a :func:`route` method to the server instance. >>> server = get_server(TcpServer, ('localhost', 502), RequestHandler) >>> server.serve_forever() :param server_class: (sub)Class of :class:`socketserver.BaseServer`. :param request_handler_class: (sub)Class of :class:`umodbus.server.RequestHandler`. :return: Instance of :param:`server_class`. """ s = server_class() s.serial_port = serial_port s.route_map = Map() s.route = MethodType(route, s) return s class AbstractSerialServer(object): _shutdown_request = False def get_meta_data(self, request_adu): """" Extract MBAP header from request adu and return it. The dict has 4 keys: transaction_id, protocol_id, length and unit_id. :param request_adu: A bytearray containing request ADU. :return: Dict with meta data of request. """ return { 'unit_id': struct.unpack('>B', request_adu[:1])[0], } def get_request_pdu(self, request_adu): """ Extract PDU from request ADU and return it. :param request_adu: A bytearray containing request ADU. :return: An bytearray container request PDU. """ return request_adu[1:-2] def serve_once(self): """ Listen and handle 1 request. """ raise NotImplementedError def serve_forever(self, poll_interval=0.5): """ Wait for incomming requests. """ self.serial_port.timeout = poll_interval while not self._shutdown_request: try: self.serve_once() except (CRCError, struct.error) as e: log.error('Can\'t handle request: {0}'.format(e)) except (SerialTimeoutException, ValueError): pass def process(self, request_adu): """ Process request ADU and return response. :param request_adu: A bytearray containing the ADU request. :return: A bytearray containing the response of the ADU request. """ meta_data = self.get_meta_data(request_adu) request_pdu = self.get_request_pdu(request_adu) response_pdu = self.execute_route(meta_data, request_pdu) response_adu = self.create_response_adu(meta_data, response_pdu) return response_adu def execute_route(self, meta_data, request_pdu): """ Execute configured route based on requests meta data and request PDU. :param meta_data: A dict with meta data. It must at least contain key 'unit_id'. :param request_pdu: A bytearray containing request PDU. :return: A bytearry containing reponse PDU. """ try: function = create_function_from_request_pdu(request_pdu) results =\ function.execute(meta_data['unit_id'], self.route_map) try: # ReadFunction's use results of callbacks to build response # PDU... return function.create_response_pdu(results) except TypeError: # ...other functions don't. return function.create_response_pdu() except ModbusError as e: function_code = get_function_code_from_request_pdu(request_pdu) return pack_exception_pdu(function_code, e.error_code) except Exception as e: log.exception('Could not handle request: {0}.'.format(e)) function_code = get_function_code_from_request_pdu(request_pdu) return pack_exception_pdu(function_code, ServerDeviceFailureError.error_code) def respond(self, response_adu): """ Send response ADU back to client. :param response_adu: A bytearray containing the response of an ADU. """ log.debug('--> {0}'.format(hexlify(response_adu))) self.serial_port.write(response_adu) def shutdown(self): self._shutdown_request = True AdvancedClimateSystems-uModbus-4fd98ba/umodbus/server/serial/rtu.py000066400000000000000000000053711372201247100256610ustar00rootroot00000000000000from __future__ import division import struct from binascii import hexlify from umodbus import log from umodbus.server.serial import AbstractSerialServer from umodbus.client.serial.redundancy_check import get_crc, validate_crc def get_char_size(baudrate): """ Get the size of 1 character in seconds. From the implementation guide: "The implementation of RTU reception driver may imply the management of a lot of interruptions due to the t 1.5 and t 3.5 timers. With high communication baud rates, this leads to a heavy CPU load. Consequently these two timers must be strictly respected when the baud rate is equal or lower than 19200 Bps. For baud rates greater than 19200 Bps, fixed values for the 2 timers should be used: it is recommended to use a value of 750us for the inter-character time-out (t 1.5) and a value of 1.750ms for inter-frame delay (t 3.5)." """ if baudrate <= 19200: # One frame is 11 bits. return 11 / baudrate # 750 us / 1.5 = 500 us or 0.0005 s. return 0.0005 class RTUServer(AbstractSerialServer): @property def serial_port(self): return self._serial_port @serial_port.setter def serial_port(self, serial_port): """ Set timeouts on serial port based on baudrate to detect frames. """ char_size = get_char_size(serial_port.baudrate) # See docstring of get_char_size() for meaning of constants below. serial_port.inter_byte_timeout = 1.5 * char_size serial_port.timeout = 3.5 * char_size self._serial_port = serial_port def serve_once(self): """ Listen and handle 1 request. """ # 256 is the maximum size of a Modbus RTU frame. request_adu = self.serial_port.read(256) log.debug('<-- {0}'.format(hexlify(request_adu))) if len(request_adu) == 0: raise ValueError response_adu = self.process(request_adu) self.respond(response_adu) def process(self, request_adu): """ Process request ADU and return response. :param request_adu: A bytearray containing the ADU request. :return: A bytearray containing the response of the ADU request. """ validate_crc(request_adu) return super(RTUServer, self).process(request_adu) def create_response_adu(self, meta_data, response_pdu): """ Build response ADU from meta data and response PDU and return it. :param meta_data: A dict with meta data. :param request_pdu: A bytearray containing request PDU. :return: A bytearray containing request ADU. """ first_part_adu = struct.pack('>B', meta_data['unit_id']) + response_pdu return first_part_adu + get_crc(first_part_adu) AdvancedClimateSystems-uModbus-4fd98ba/umodbus/server/tcp.py000066400000000000000000000052061372201247100243530ustar00rootroot00000000000000import struct from types import MethodType from umodbus.route import Map from umodbus.server import AbstractRequestHandler, route from umodbus.utils import unpack_mbap, pack_mbap from umodbus.exceptions import ServerDeviceFailureError def get_server(server_class, server_address, request_handler_class): """ Return instance of :param:`server_class` with :param:`request_handler` bound to it. This method also binds a :func:`route` method to the server instance. >>> server = get_server(TcpServer, ('localhost', 502), RequestHandler) >>> server.serve_forever() :param server_class: (sub)Class of :class:`socketserver.BaseServer`. :param request_handler_class: (sub)Class of :class:`umodbus.server.RequestHandler`. :return: Instance of :param:`server_class`. """ s = server_class(server_address, request_handler_class) s.route_map = Map() s.route = MethodType(route, s) return s class RequestHandler(AbstractRequestHandler): """ A subclass of :class:`socketserver.BaseRequestHandler` dispatching incoming Modbus TCP/IP request using the server's :attr:`route_map`. """ def get_meta_data(self, request_adu): """" Extract MBAP header from request adu and return it. The dict has 4 keys: transaction_id, protocol_id, length and unit_id. :param request_adu: A bytearray containing request ADU. :return: Dict with meta data of request. """ try: transaction_id, protocol_id, length, unit_id = \ unpack_mbap(request_adu[:7]) except struct.error: raise ServerDeviceFailureError() return { 'transaction_id': transaction_id, 'protocol_id': protocol_id, 'length': length, 'unit_id': unit_id, } def get_request_pdu(self, request_adu): """ Extract PDU from request ADU and return it. :param request_adu: A bytearray containing request ADU. :return: An bytearray container request PDU. """ return request_adu[7:] def create_response_adu(self, meta_data, response_pdu): """ Build response ADU from meta data and response PDU and return it. :param meta_data: A dict with meta data. :param request_pdu: A bytearray containing request PDU. :return: A bytearray containing request ADU. """ response_mbap = pack_mbap( transaction_id=meta_data['transaction_id'], protocol_id=meta_data['protocol_id'], length=len(response_pdu) + 1, unit_id=meta_data['unit_id'] ) return response_mbap + response_pdu AdvancedClimateSystems-uModbus-4fd98ba/umodbus/utils.py000066400000000000000000000113171372201247100234170ustar00rootroot00000000000000import sys import struct import logging from logging import StreamHandler, Formatter from functools import wraps from umodbus import log def log_to_stream(stream=sys.stderr, level=logging.NOTSET, fmt=logging.BASIC_FORMAT): """ Add :class:`logging.StreamHandler` to logger which logs to a stream. :param stream. Stream to log to, default STDERR. :param level: Log level, default NOTSET. :param fmt: String with log format, default is BASIC_FORMAT. """ fmt = Formatter(fmt) handler = StreamHandler() handler.setFormatter(fmt) handler.setLevel(level) log.addHandler(handler) def unpack_mbap(mbap): """ Parse MBAP of 7 bytes and return tuple with fields. >>> parse_mbap(b'\x00\x08\x00\x00\x00\x06\x01') (8, 0, 6, 1) :param mbap: Array of 7 bytes. :return: Tuple with 4 values: Transaction identifier, Protocol identifier, Length and Unit identifier. """ # '>' indicates data is big-endian. Modbus uses this alignment. 'H' and 'B' # are format characters. 'H' is unsigned short of 2 bytes. 'B' is an # unsigned char of 1 byte. HHHB sums up to 2 + 2 + 2 + 1 = 7 bytes. # TODO What it right exception to raise? Error code 04, Server failure, # seems most appropriate. return struct.unpack('>HHHB', mbap) def pack_mbap(transaction_id, protocol_id, length, unit_id): """ Create and return response MBAP. :param transaction_id: Transaction id. :param protocol_id: Protocol id. :param length: Length of following bytes in ADU. :param unit_id: Unit id. :return: Byte array of 7 bytes. """ return struct.pack('>HHHB', transaction_id, protocol_id, length, unit_id) def pack_exception_pdu(function_code, error_code): """ Return exception PDU of 2 bytes. "The exception response message has two fields that differentiate it from a nor mal response: Function Code Field: In a normal response, the server echoes the function code of the original request in the function code field of the response. All function codes have a most - significant bit (MSB) of 0 (their values are all below 80 hexadecimal). In an exception response, the server sets the MSB of the function code to 1. This makes the function code value in an exception response exactly 80 hexadecimal higher than the value would be for a normal response. With the function code's MSB set, the client's application program can recognize the exception response and can examine the data field for the exception code. Data Field: In a normal response, the server may return data or statistics in the data field (any information that was requested in the request). In an exception response, the server returns an exception code in the data field. This defines the server condition that caused the exception." -- MODBUS Application Protocol Specification V1.1b3, chapter 7 ================ =============== Field Length (bytes) ================ =============== Error code 1 Function code 1 ================ =============== :param error_code: Error code. :param function_code: Function code. :return: PDU of 2 bytes. """ return struct.pack('>BB', function_code + 0x80, error_code) def get_function_code_from_request_pdu(pdu): """ Return function code from request PDU. :return pdu: Array with bytes. :return: Function code. """ return struct.unpack('>B', pdu[:1])[0] def memoize(f): """ Decorator which caches function's return value each it is called. If called later with same arguments, the cached value is returned. """ cache = {} @wraps(f) def inner(arg): if arg not in cache: cache[arg] = f(arg) return cache[arg] return inner def recv_exactly(recv_fn, size): """ Use the function to read and return exactly number of bytes desired. https://docs.python.org/3/howto/sockets.html#socket-programming-howto for more information about why this is necessary. :param recv_fn: Function that can return up to given bytes (i.e. socket.recv, file.read) :param size: Number of bytes to read. :return: Byte string with length size. :raises ValueError: Could not receive enough data (usually timeout). """ recv_bytes = 0 chunks = [] while recv_bytes < size: chunk = recv_fn(size - recv_bytes) if len(chunk) == 0: # when closed or empty break recv_bytes += len(chunk) chunks.append(chunk) response = b''.join(chunks) if len(response) != size: raise ValueError return response