From d51b1932c013610d3d795650329e950557b43e31 Mon Sep 17 00:00:00 2001 From: Gijs Molenaar Date: Wed, 1 May 2024 17:17:45 +0200 Subject: [PATCH] Prepare for 1.4 (#477) * cleanup and restructure * enable python 3.12 * fix typing * update most EOL deps * all failing checks hopefully pass now * update precommit hook versions * fixing some problems * fix ruff error * again fix all problems --- .github/actions/linux_armv7l/Dockerfile | 6 - .../actions/manylinux_2_24_aarch64/action.yml | 28 - .../manylinux_2_24_x86_64/entrypoint.sh | 7 - .../Dockerfile | 2 +- .../action.yml | 4 +- .../entrypoint.sh | 0 .../Dockerfile | 2 +- .../action.yml | 4 +- .../entrypoint.sh | 0 .github/workflows/build-and-test-arm32v7.yml | 67 - .github/workflows/build-and-test-arm64.yml | 26 +- .github/workflows/build-and-test.yml | 34 +- .github/workflows/doc.yml | 2 +- .github/workflows/docker.yml | 8 +- .github/workflows/linux.yml | 8 +- .github/workflows/mypy.yml | 2 +- .github/workflows/osx.yml | 6 +- .github/workflows/pre-commit.yml | 8 +- .github/workflows/test-pypi-packages.yml | 8 +- .github/workflows/windows.yml | 4 +- .gitignore | 1 + .pre-commit-config.yaml | 6 +- Makefile | 11 +- doc/conf.py | 138 +- doc/make.bat | 190 -- example/boolean.py | 18 +- example/example.py | 85 +- example/logo_7_8.py | 2 +- example/read_multi.py | 10 +- example/write_multi.py | 10 +- pyproject.toml | 38 +- snap7/__init__.py | 9 +- snap7/client/__init__.py | 146 +- snap7/common.py | 45 +- snap7/error.py | 154 +- snap7/logo.py | 20 +- snap7/partner.py | 22 +- snap7/server/__init__.py | 127 +- snap7/server/__main__.py | 5 +- snap7/types.py | 239 ++- snap7/util.py | 1880 ----------------- snap7/util/__init__.py | 200 ++ snap7/util/db.py | 598 ++++++ snap7/util/getters.py | 719 +++++++ snap7/util/setters.py | 510 +++++ tests/bla.py | 3 + tests/test_client.py | 205 +- tests/test_common.py | 3 +- tests/test_logo_client.py | 20 +- tests/test_mainloop.py | 44 +- tests/test_partner.py | 12 +- tests/test_server.py | 13 +- tests/test_util.py | 559 ++--- 53 files changed, 3054 insertions(+), 3214 deletions(-) delete mode 100644 .github/actions/linux_armv7l/Dockerfile delete mode 100644 .github/actions/manylinux_2_24_aarch64/action.yml delete mode 100755 .github/actions/manylinux_2_24_x86_64/entrypoint.sh rename .github/actions/{manylinux_2_24_aarch64 => manylinux_2_28_aarch64}/Dockerfile (66%) rename .github/actions/{linux_armv7l => manylinux_2_28_aarch64}/action.yml (89%) rename .github/actions/{linux_armv7l => manylinux_2_28_aarch64}/entrypoint.sh (100%) rename .github/actions/{manylinux_2_24_x86_64 => manylinux_2_28_x86_64}/Dockerfile (67%) rename .github/actions/{manylinux_2_24_x86_64 => manylinux_2_28_x86_64}/action.yml (89%) rename .github/actions/{manylinux_2_24_aarch64 => manylinux_2_28_x86_64}/entrypoint.sh (100%) delete mode 100644 .github/workflows/build-and-test-arm32v7.yml delete mode 100644 doc/make.bat delete mode 100644 snap7/util.py create mode 100644 snap7/util/__init__.py create mode 100644 snap7/util/db.py create mode 100644 snap7/util/getters.py create mode 100644 snap7/util/setters.py create mode 100644 tests/bla.py diff --git a/.github/actions/linux_armv7l/Dockerfile b/.github/actions/linux_armv7l/Dockerfile deleted file mode 100644 index 6cb05aa7..00000000 --- a/.github/actions/linux_armv7l/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM ghcr.io/nikteliy/manylinux_2_24_armv7l:python3.7 - -COPY entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh - -ENTRYPOINT ["/entrypoint.sh"] diff --git a/.github/actions/manylinux_2_24_aarch64/action.yml b/.github/actions/manylinux_2_24_aarch64/action.yml deleted file mode 100644 index 8235ee53..00000000 --- a/.github/actions/manylinux_2_24_aarch64/action.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: 'manylinux_2_24_aarch64' -description: 'Builds manylinux_2_24_aarch64 package' -inputs: - script: - description: 'Specifies the path to the build script' - required: true - platform: - description: 'Specifies the --plat-name option to the build command' - required: true - makefile: - description: 'Specifies the path to the .mk file' - required: true - python: - description: 'Specifies the path to the python interpreter' - default: /usr/bin/python3 - wheeldir: - description: 'Specifies directory to store delocated wheels' - required: true - default: wheelhouse -runs: - using: 'docker' - image: 'Dockerfile' - args: - - ${{ inputs.script }} - - ${{ inputs.platform }} - - ${{ inputs.makefile }} - - ${{ inputs.python }} - - ${{ inputs.wheeldir }} diff --git a/.github/actions/manylinux_2_24_x86_64/entrypoint.sh b/.github/actions/manylinux_2_24_x86_64/entrypoint.sh deleted file mode 100755 index 000725cb..00000000 --- a/.github/actions/manylinux_2_24_x86_64/entrypoint.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -set -o errexit -set -o pipefail -set -o nounset - -exec "$INPUT_SCRIPT" diff --git a/.github/actions/manylinux_2_24_aarch64/Dockerfile b/.github/actions/manylinux_2_28_aarch64/Dockerfile similarity index 66% rename from .github/actions/manylinux_2_24_aarch64/Dockerfile rename to .github/actions/manylinux_2_28_aarch64/Dockerfile index 5c304e38..0a7245a5 100644 --- a/.github/actions/manylinux_2_24_aarch64/Dockerfile +++ b/.github/actions/manylinux_2_28_aarch64/Dockerfile @@ -1,4 +1,4 @@ -FROM quay.io/pypa/manylinux_2_24_aarch64:latest +FROM quay.io/pypa/manylinux_2_28_aarch64:latest COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh diff --git a/.github/actions/linux_armv7l/action.yml b/.github/actions/manylinux_2_28_aarch64/action.yml similarity index 89% rename from .github/actions/linux_armv7l/action.yml rename to .github/actions/manylinux_2_28_aarch64/action.yml index 937006ea..f37595fd 100644 --- a/.github/actions/linux_armv7l/action.yml +++ b/.github/actions/manylinux_2_28_aarch64/action.yml @@ -1,5 +1,5 @@ -name: 'linux_armv7l' -description: 'Builds linux_armv7l package' +name: 'manylinux_2_28_aarch64' +description: 'Builds manylinux_2_28_aarch64 package' inputs: script: description: 'Specifies the path to the build script' diff --git a/.github/actions/linux_armv7l/entrypoint.sh b/.github/actions/manylinux_2_28_aarch64/entrypoint.sh similarity index 100% rename from .github/actions/linux_armv7l/entrypoint.sh rename to .github/actions/manylinux_2_28_aarch64/entrypoint.sh diff --git a/.github/actions/manylinux_2_24_x86_64/Dockerfile b/.github/actions/manylinux_2_28_x86_64/Dockerfile similarity index 67% rename from .github/actions/manylinux_2_24_x86_64/Dockerfile rename to .github/actions/manylinux_2_28_x86_64/Dockerfile index 1460d38e..29fa8881 100644 --- a/.github/actions/manylinux_2_24_x86_64/Dockerfile +++ b/.github/actions/manylinux_2_28_x86_64/Dockerfile @@ -1,4 +1,4 @@ -FROM quay.io/pypa/manylinux_2_24_x86_64:latest +FROM quay.io/pypa/manylinux_2_28_x86_64:latest COPY /entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh diff --git a/.github/actions/manylinux_2_24_x86_64/action.yml b/.github/actions/manylinux_2_28_x86_64/action.yml similarity index 89% rename from .github/actions/manylinux_2_24_x86_64/action.yml rename to .github/actions/manylinux_2_28_x86_64/action.yml index 72688a51..580191f4 100644 --- a/.github/actions/manylinux_2_24_x86_64/action.yml +++ b/.github/actions/manylinux_2_28_x86_64/action.yml @@ -1,5 +1,5 @@ -name: 'manylinux_2_24_x86_64' -description: 'Builds manylinux_2_24_x86_64 package' +name: 'manylinux_2_28_x86_64' +description: 'Builds manylinux_2_28_x86_64 package' inputs: script: description: 'Specifies the path to the build script' diff --git a/.github/actions/manylinux_2_24_aarch64/entrypoint.sh b/.github/actions/manylinux_2_28_x86_64/entrypoint.sh similarity index 100% rename from .github/actions/manylinux_2_24_aarch64/entrypoint.sh rename to .github/actions/manylinux_2_28_x86_64/entrypoint.sh diff --git a/.github/workflows/build-and-test-arm32v7.yml b/.github/workflows/build-and-test-arm32v7.yml deleted file mode 100644 index 5c91caa8..00000000 --- a/.github/workflows/build-and-test-arm32v7.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: build-and-test-wheels-arm32 -on: - push: - branches: [master] - pull_request: - branches: [master] -jobs: - linux-build-arm32v7: - name: Build arm32 wheel - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Prepare snap7 archive - uses: ./.github/actions/prepare_snap7 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - with: - platforms: arm - - - name: Build wheel - uses: ./.github/actions/linux_armv7l - with: - script: ./.github/build_scripts/build_package.sh - platform: manylinux_2_24_armv7l - makefile: arm_v7_linux.mk - python: /usr/local/bin/python3 - - - name: Upload artifacts - uses: actions/upload-artifact@v3 - with: - name: wheels - path: wheelhouse/*.whl - - test-wheels-arm32: - name: Testing wheel - needs: linux-build-arm32v7 - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Download artifacts - uses: actions/download-artifact@v3 - with: - name: wheels - path: wheelhouse - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - with: - platforms: arm - - - name: Run tests in docker:arm32v7 - run: | - docker run --platform linux/arm/v7 --rm --interactive -v $PWD/tests:/tests \ - -v $PWD/pyproject.toml:/pyproject.toml \ - -v $PWD/wheelhouse:/wheelhouse \ - "arm32v7/python:${{ matrix.python-version }}-buster" /bin/bash -s < v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# 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 +# 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 +# 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'] +html_static_path = ["_static"] # 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' +# 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 +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = 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 = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'python-snap7doc' +htmlhelp_basename = "python-snap7doc" # -- 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': '', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'python-snap7.tex', 'python-snap7 Documentation', - 'Gijs Molenaar, Stephan Preeker', 'manual'), + ("index", "python-snap7.tex", "python-snap7 Documentation", "Gijs Molenaar, Stephan Preeker", "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# 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 = [ - ('index', 'python-snap7', 'python-snap7 Documentation', - ['Gijs Molenaar, Stephan Preeker'], 1) -] +man_pages = [("index", "python-snap7", "python-snap7 Documentation", ["Gijs Molenaar, Stephan Preeker"], 1)] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ @@ -229,19 +223,25 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'python-snap7', 'python-snap7 Documentation', - 'Gijs Molenaar, Stephan Preeker', 'python-snap7', 'One line description of project.', - 'Miscellaneous'), + ( + "index", + "python-snap7", + "python-snap7 Documentation", + "Gijs Molenaar, Stephan Preeker", + "python-snap7", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # Napoleon settings diff --git a/doc/make.bat b/doc/make.bat deleted file mode 100644 index 720654cf..00000000 --- a/doc/make.bat +++ /dev/null @@ -1,190 +0,0 @@ -@ECHO OFF - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set BUILDDIR=_build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . -set I18NSPHINXOPTS=%SPHINXOPTS% . -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% -) - -if "%1" == "" goto help - -if "%1" == "help" ( - :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. 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. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\python-snap7.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\python-snap7.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - -:end diff --git a/example/boolean.py b/example/boolean.py index 39ec278b..47343849 100644 --- a/example/boolean.py +++ b/example/boolean.py @@ -16,29 +16,31 @@ the minimun amount of data being read or written to a plc is 1 byte. """ + import snap7 +import snap7.util.setters plc = snap7.client.Client() -plc.connect('192.168.200.24', 0, 3) +plc.connect("192.168.200.24", 0, 3) # In this example boolean in DB 31 at byte 120 and bit 5 is changed. = 120.5 -reading = plc.db_read(31, 120, 1) # read 1 byte from db 31 staring from byte 120 -snap7.util.set_bool(reading, 0, 5) # set a value of fifth bit -plc.db_write(reading, 31, 120, 1) # write back the bytearray and now the boolean value is changed in the PLC. +reading = plc.db_read(31, 120, 1) # read 1 byte from db 31 staring from byte 120 +snap7.util.setters.set_bool(reading, 0, 5) # set a value of fifth bit +plc.db_write(reading, 31, 120, 1) # write back the bytearray and now the boolean value is changed in the PLC. # NOTE you could also use the read_area and write_area functions. # then you can specify an area to read from: # https://github.com/gijzelaerr/python-snap7/blob/master/snap7/types.py -from snap7.types import areas # noqa: E402 +from snap7.types import areas # noqa: E402 # play with these functions. -plc.read_area(area=areas['MK'], dbnumber=0, start=20, size=2) +plc.read_area(area=areas["MK"], dbnumber=0, start=20, size=2) data = bytearray() -snap7.util.set_int(data, 0, 127) -plc.write_area(area=areas['MK'], dbnumber=0, start=20, data=data) +snap7.util.setters.set_int(data, 0, 127) +plc.write_area(area=areas["MK"], dbnumber=0, start=20, data=data) # read the client source code! # and official snap7 documentation diff --git a/example/example.py b/example/example.py index e8df9e74..3c886c77 100644 --- a/example/example.py +++ b/example/example.py @@ -1,5 +1,6 @@ import time +import snap7.util.db from db_layouts import rc_if_db_1_layout from db_layouts import tank_rc_if_db_layout @@ -19,7 +20,7 @@ """) client = snap7.client.Client() -client.connect('192.168.200.24', 0, 3) +client.connect("192.168.200.24", 0, 3) def get_db1(): @@ -29,10 +30,10 @@ def get_db1(): """ all_data = client.db_get(1) - for i in range(400): # items in db - row_size = 130 # size of item + for i in range(400): # items in db + row_size = 130 # size of item index = i * row_size - offset = index + row_size # end of row in db + offset = index + row_size # end of row in db util.print_row(all_data[index:offset]) @@ -73,10 +74,9 @@ def show_row(x): while True: data = get_db_row(1, 4 + x * row_size, row_size) - row = snap7.util.DB_Row(data, rc_if_db_1_layout, - layout_offset=4) - print('name', row['RC_IF_NAME']) - print(row['RC_IF_NAME']) + row = snap7.util.db.DB_Row(data, rc_if_db_1_layout, layout_offset=4) + print("name", row["RC_IF_NAME"]) + print(row["RC_IF_NAME"]) break # do some write action.. @@ -86,9 +86,7 @@ def show_row(x): def get_row(x): row_size = 126 data = get_db_row(1, 4 + x * row_size, row_size) - row = snap7.util.DB_Row( - data, rc_if_db_1_layout, - layout_offset=4) + row = snap7.util.db.DB_Row(data, rc_if_db_1_layout, layout_offset=4) return row @@ -108,14 +106,14 @@ def open_row(row): """ # row['AutAct'] = 1 - row['Occupied'] = 1 - row['BatchName'] = 'test' - row['AutModLi'] = 1 - row['ManModLi'] = 0 - row['ModLiOp'] = 1 + row["Occupied"] = 1 + row["BatchName"] = "test" + row["AutModLi"] = 1 + row["ManModLi"] = 0 + row["ModLiOp"] = 1 - row['CloseAut'] = 0 - row['OpenAut'] = 1 + row["CloseAut"] = 0 + row["OpenAut"] = 1 # row['StartAut'] = True # row['StopAut'] = False @@ -128,10 +126,11 @@ def close_row(row): close a valve """ # print row['RC_IF_NAME'] - row['BatchName'] = '' - row['Occupied'] = 0 - row['CloseAut'] = 1 - row['OpenAut'] = 0 + row["BatchName"] = "" + row["Occupied"] = 0 + row["CloseAut"] = 1 + row["OpenAut"] = 0 + # show_row(0) # show_row(1) @@ -152,7 +151,7 @@ def open_and_close(): def set_part_db(start, size, _bytearray): - data = _bytearray[start:start + size] + data = _bytearray[start : start + size] set_db_row(1, start, size, data) @@ -166,7 +165,7 @@ def open_and_close_db1(): t = time.time() db1 = make_item_db(1) all_data = db1._bytearray - print(f'row objects: {len(db1.index)}') + print(f"row objects: {len(db1.index)}") for x, (name, row) in enumerate(db1.index.items()): open_row(row) @@ -174,9 +173,9 @@ def open_and_close_db1(): t = time.time() write_data_db(1, all_data, 4 + 126 * 450) - print(f'opening all valves took: {time.time() - t}') + print(f"opening all valves took: {time.time() - t}") - print('sleep...') + print("sleep...") time.sleep(5) for x, (name, row) in enumerate(db1): close_row(row) @@ -186,7 +185,7 @@ def open_and_close_db1(): t = time.time() write_data_db(1, all_data, 4 + 126 * 450) - print(f'closing all valves took: {time.time() - t}') + print(f"closing all valves took: {time.time() - t}") def read_tank_db(): @@ -200,18 +199,18 @@ def make_item_db(db_number): t = time.time() all_data = client.db_upload(db_number) - print(f'getting all data took: {time.time() - t}') - - db1 = snap7.util.DB( - db_number, # the db we use - all_data, # bytearray from the plc - rc_if_db_1_layout, # layout specification - 126, # size of the specification - 450, # number of row's / specifocations - id_field='RC_IF_NAME', # field we can use to make row - layout_offset=4, # sometimes specification does not start a 0 - db_offset=4 # At which point in all_data should we start - # parsing for data + print(f"getting all data took: {time.time() - t}") + + db1 = snap7.util.db.DB( + db_number, # the db we use + all_data, # bytearray from the plc + rc_if_db_1_layout, # layout specification + 126, # size of the specification + 450, # number of row's / specifocations + id_field="RC_IF_NAME", # field we can use to make row + layout_offset=4, # sometimes specification does not start a 0 + db_offset=4, # At which point in all_data should we start + # parsing for data ) return db1 @@ -219,21 +218,19 @@ def make_item_db(db_number): def make_tank_db(): tank_data = client.db_upload(73) - db73 = snap7.util.DB( - 73, tank_data, tank_rc_if_db_layout, - 238, 2, id_field='RC_IF_NAME') + db73 = snap7.util.db.DB(73, tank_data, tank_rc_if_db_layout, 238, 2, id_field="RC_IF_NAME") return db73 def print_tag(): db1 = make_item_db(1) - print(db1['5V315']) + print(db1["5V315"]) def print_open(): db1 = make_item_db(1) for x, (name, row) in enumerate(db1): - if row['BatchName']: + if row["BatchName"]: print(row) diff --git a/example/logo_7_8.py b/example/logo_7_8.py index 4db4d2f9..f7903f25 100644 --- a/example/logo_7_8.py +++ b/example/logo_7_8.py @@ -20,7 +20,7 @@ logger.info("connected") # read I1 from logo - vm_address = ("V923.0" if Logo_7 else "V1024.0") + vm_address = "V923.0" if Logo_7 else "V1024.0" print(f"I1: {str(plc.read(vm_address))}") # write some values in VM addresses between 0 and 100 diff --git a/example/read_multi.py b/example/read_multi.py index 6050b2cf..06e951c4 100644 --- a/example/read_multi.py +++ b/example/read_multi.py @@ -6,13 +6,12 @@ import ctypes -import snap7 +import snap7.util.getters from snap7.common import check_error from snap7.types import S7DataItem, S7AreaDB, S7WLByte -from snap7 import util client = snap7.client.Client() -client.connect('10.100.5.2', 0, 2) +client.connect("10.100.5.2", 0, 2) data_items = (S7DataItem * 3)() @@ -44,8 +43,7 @@ buffer = ctypes.create_string_buffer(di.Amount) # cast the pointer to the buffer to the required type - pBuffer = ctypes.cast(ctypes.pointer(buffer), - ctypes.POINTER(ctypes.c_uint8)) + pBuffer = ctypes.cast(ctypes.pointer(buffer), ctypes.POINTER(ctypes.c_uint8)) di.pData = pBuffer result, data_items = client.read_multi_vars(data_items) @@ -55,7 +53,7 @@ result_values = [] # function to cast bytes to match data_types[] above -byte_to_value = [util.get_real, util.get_real, util.get_int] +byte_to_value = [snap7.util.getters.get_real, snap7.util.getters.get_real, snap7.util.getters.get_int] # unpack and test the result of each read for i in range(0, len(data_items)): diff --git a/example/write_multi.py b/example/write_multi.py index de55d742..4f48309a 100644 --- a/example/write_multi.py +++ b/example/write_multi.py @@ -5,7 +5,7 @@ client = snap7.client.Client() -client.connect('192.168.100.100', 0, 2) +client.connect("192.168.100.100", 0, 2) items = [] @@ -31,7 +31,7 @@ def set_data_item(area, word_len, db_number: int, start: int, amount: int, data: real = bytearray(4) set_real(real, 0, 42.5) -counters = 0x2999.to_bytes(2, 'big') + 0x1111.to_bytes(2, 'big') +counters = 0x2999.to_bytes(2, "big") + 0x1111.to_bytes(2, "big") item1 = set_data_item(area=Areas.DB, word_len=S7WLWord, db_number=1, start=0, amount=4, data=ints) item2 = set_data_item(area=Areas.DB, word_len=S7WLReal, db_number=1, start=8, amount=1, data=real) @@ -47,6 +47,6 @@ def set_data_item(area, word_len, db_number: int, start: int, amount: int, data: db_real = client.db_read(1, 8, 12) db_counters = client.ct_read(2, 2) -print(f'int values: {[get_int(db_int, i * 2) for i in range(4)]}') -print(f'real value: {get_real(db_real, 0)}') -print(f'counters: {get_s5time(counters, 0)}, {get_s5time(counters, 2)}') +print(f"int values: {[get_int(db_int, i * 2) for i in range(4)]}") +print(f"real value: {get_real(db_real, 0)}") +print(f"counters: {get_s5time(counters, 0)}, {get_s5time(counters, 2)}") diff --git a/pyproject.toml b/pyproject.toml index 801e3db2..93d5b899 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta" [project] name = "python-snap7" -version = "1.3" +version = "1.4" description = "Python wrapper for the snap7 library" authors = [ - {name = "Gijs Molenaar", email = "gijs@pythonic.nl"}, + {name = "Gijs Molenaar", email = "gijsmolenaar@gmail.com"}, ] classifiers = [ "Development Status :: 5 - Production/Stable", @@ -17,32 +17,35 @@ classifiers = [ "Intended Audience :: Manufacturing", "License :: OSI Approved :: MIT License", "Operating System :: POSIX", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] license = {text = "MIT"} -requires-python = ">=3.7" +requires-python = ">=3.8" [project.urls] Homepage = "https://github.com/gijzelaerr/python-snap7" Documentation = "https://python-snap7.readthedocs.io/en/latest/" [project.optional-dependencies] -test = ["pytest", "pytest-asyncio", "mypy", "types-setuptools", "ruff"] +test = ["pytest", "mypy", "types-setuptools", "ruff"] cli = ["rich", "click" ] doc = ["sphinx", "sphinx_rtd_theme"] [tool.setuptools.package-data] snap7 = ["py.typed", "lib/libsnap7.so", "lib/snap7.dll", "lib/libsnap7.dylib"] +[tool.setuptools.packages.find] +where = ["."] +include = ["snap7"] + [project.scripts] snap7-server = "snap7.server.__main__:main" [tool.pytest.ini_options] -asyncio_mode = "auto" testpaths = ["tests"] markers =[ "client", @@ -58,23 +61,10 @@ markers =[ ignore_missing_imports = true [tool.ruff] -select = [ - "E", - "F", - "UP", - "YTT", - "ASYNC", - "S", - "A", - "PIE", - "PYI", - "PTH", - "C90", -] -show-source = true +output-format = "full" line-length = 130 -ignore = [] -target-version = "py37" +target-version = "py38" -[tool.ruff.mccabe] -max-complexity = 10 +[lint] +ignore = [] +mccabe.max-complexity = 10 diff --git a/snap7/__init__.py b/snap7/__init__.py index c24951fa..36c6e0ee 100644 --- a/snap7/__init__.py +++ b/snap7/__init__.py @@ -1,7 +1,8 @@ """ The Snap7 Python library. """ -import pkg_resources + +from importlib.metadata import version, PackageNotFoundError from . import client from . import common @@ -11,9 +12,9 @@ from . import types from . import util -__all__ = ['client', 'common', 'error', 'logo', 'server', 'types', 'util'] +__all__ = ["client", "common", "error", "logo", "server", "types", "util"] try: - __version__ = pkg_resources.require("python-snap7")[0].version -except pkg_resources.DistributionNotFound: + __version__ = version("python-snap7") +except PackageNotFoundError: __version__ = "0.0rc0" diff --git a/snap7/client/__init__.py b/snap7/client/__init__.py index 9022439c..e6921bab 100644 --- a/snap7/client/__init__.py +++ b/snap7/client/__init__.py @@ -1,6 +1,7 @@ """ Snap7 client used for connection to a siemens 7 server. """ + import re import logging from ctypes import byref, create_string_buffer, sizeof @@ -13,6 +14,7 @@ from ..types import S7OrderCode, S7Protection, S7SZLList, TS7BlockInfo, WordLen from ..types import S7Object, buffer_size, buffer_type, cpu_statuses, param_types from ..types import RemotePort, wordlen_to_ctypes, block_types + logger = logging.getLogger(__name__) @@ -68,8 +70,7 @@ def __del__(self): self.destroy() def create(self): - """Creates a SNAP7 client. - """ + """Creates a SNAP7 client.""" logger.info("creating snap7 client") self._library.Cli_Create.restype = c_void_p self._pointer = S7Object(self._library.Cli_Create()) @@ -190,9 +191,7 @@ def connect(self, address: str, rack: int, slot: int, tcpport: int = 102) -> int logger.info(f"connecting to {address}:{tcpport} rack {rack} slot {slot}") self.set_param(RemotePort, tcpport) - return self._library.Cli_ConnectTo( - self._pointer, c_char_p(address.encode()), - c_int(rack), c_int(slot)) + return self._library.Cli_ConnectTo(self._pointer, c_char_p(address.encode()), c_int(rack), c_int(slot)) def db_read(self, db_number: int, start: int, size: int) -> bytearray: """Reads a part of a DB from a PLC @@ -220,9 +219,7 @@ def db_read(self, db_number: int, start: int, size: int) -> bytearray: type_ = wordlen_to_ctypes[WordLen.Byte.value] data = (type_ * size)() - result = (self._library.Cli_DBRead( - self._pointer, db_number, start, size, - byref(data))) + result = self._library.Cli_DBRead(self._pointer, db_number, start, size, byref(data)) check_error(result, context="client") return bytearray(data) @@ -250,8 +247,7 @@ def db_write(self, db_number: int, start: int, data: bytearray) -> int: size = len(data) cdata = (type_ * size).from_buffer_copy(data) logger.debug(f"db_write db_number:{db_number} start:{start} size:{size} data:{data}") - return self._library.Cli_DBWrite(self._pointer, db_number, start, size, - byref(cdata)) + return self._library.Cli_DBWrite(self._pointer, db_number, start, size, byref(cdata)) def delete(self, block_type: str, block_num: int) -> int: """Delete a block into AG. @@ -283,11 +279,9 @@ def full_upload(self, _type: str, block_num: int) -> Tuple[bytearray, int]: _buffer = buffer_type() size = c_int(sizeof(_buffer)) block_type = block_types[_type] - result = self._library.Cli_FullUpload(self._pointer, block_type, - block_num, byref(_buffer), - byref(size)) + result = self._library.Cli_FullUpload(self._pointer, block_type, block_num, byref(_buffer), byref(size)) check_error(result, context="client") - return bytearray(_buffer)[:size.value], size.value + return bytearray(_buffer)[: size.value], size.value def upload(self, block_num: int) -> bytearray: """Uploads a block from AG. @@ -302,15 +296,14 @@ def upload(self, block_num: int) -> bytearray: Buffer with the uploaded block. """ logger.debug(f"db_upload block_num: {block_num}") - block_type = block_types['DB'] + block_type = block_types["DB"] _buffer = buffer_type() size = c_int(sizeof(_buffer)) - result = self._library.Cli_Upload(self._pointer, block_type, block_num, - byref(_buffer), byref(size)) + result = self._library.Cli_Upload(self._pointer, block_type, block_num, byref(_buffer), byref(size)) check_error(result, context="client") - logger.info(f'received {size} bytes') + logger.info(f"received {size} bytes") return bytearray(_buffer) @error_wrap @@ -332,8 +325,7 @@ def download(self, data: bytearray, block_num: int = -1) -> int: type_ = c_byte size = len(data) cdata = (type_ * len(data)).from_buffer_copy(data) - return self._library.Cli_Download(self._pointer, block_num, - byref(cdata), size) + return self._library.Cli_Download(self._pointer, block_num, byref(cdata), size) def db_get(self, db_number: int) -> bytearray: """Uploads a DB from AG using DBRead. @@ -357,35 +349,33 @@ def db_get(self, db_number: int) -> bytearray: """ logger.debug(f"db_get db_number: {db_number}") _buffer = buffer_type() - result = self._library.Cli_DBGet( - self._pointer, db_number, byref(_buffer), - byref(c_int(buffer_size))) + result = self._library.Cli_DBGet(self._pointer, db_number, byref(_buffer), byref(c_int(buffer_size))) check_error(result, context="client") return bytearray(_buffer) def read_area(self, area: Areas, dbnumber: int, start: int, size: int) -> bytearray: """Reads a data area from a PLC - With it you can read DB, Inputs, Outputs, Merkers, Timers and Counters. + With it you can read DB, Inputs, Outputs, Merkers, Timers and Counters. - Args: - area: area to be read from. - dbnumber: number of the db to be read from. In case of Inputs, Marks or Outputs, this should be equal to 0. - start: byte index to start reading. - size: number of bytes to read. + Args: + area: area to be read from. + dbnumber: number of the db to be read from. In case of Inputs, Marks or Outputs, this should be equal to 0. + start: byte index to start reading. + size: number of bytes to read. - Returns: - Buffer with the data read. + Returns: + Buffer with the data read. - Raises: - :obj:`ValueError`: if the area is not defined in the `Areas` + Raises: + :obj:`ValueError`: if the area is not defined in the `Areas` - Example: - >>> import snap7 - >>> client = snap7.client.Client() - >>> client.connect("192.168.0.1", 0, 0) - >>> buffer = client.read_area(Areas.DB, 1, 10, 4) # Reads the DB number 1 from the byte 10 to the byte 14. - >>> buffer - bytearray(b'\\x00\\x00') + Example: + import snap7.util.db >>> import snap7 + >>> client = snap7.client.Client() + >>> client.connect("192.168.0.1", 0, 0) + >>> buffer = client.read_area(snap7.util.db.DB, 1, 10, 4) # Reads the DB number 1 from the byte 10 to the byte 14. + >>> buffer + bytearray(b'\\x00\\x00') """ if area not in Areas: raise ValueError(f"{area} is not implemented in types") @@ -399,10 +389,9 @@ def read_area(self, area: Areas, dbnumber: int, start: int, size: int) -> bytear logger.debug( f"reading area: {area.name} dbnumber: {dbnumber} start: {start} amount: {size} " f"wordlen: {wordlen.name}={wordlen.value}" - ) + ) data = (type_ * size)() - result = self._library.Cli_ReadArea(self._pointer, area.value, dbnumber, start, - size, wordlen.value, byref(data)) + result = self._library.Cli_ReadArea(self._pointer, area.value, dbnumber, start, size, wordlen.value, byref(data)) check_error(result, context="client") return bytearray(data) @@ -420,11 +409,13 @@ def write_area(self, area: Areas, dbnumber: int, start: int, data: bytearray) -> Snap7 error code. Exmaple: + >>> import snap7.util.db >>> import snap7 >>> client = snap7.client.Client() >>> client.connect("192.168.0.1", 0, 0) >>> buffer = bytearray([0b00000001]) - >>> client.write_area(Areas.DB, 1, 10, buffer) # Writes the bit 0 of the byte 10 from the DB number 1 to TRUE. + # Writes the bit 0 of the byte 10 from the DB number 1 to TRUE. + >>> client.write_area(snap7.util.DB, 1, 10, buffer) """ if area == Areas.TM: wordlen = WordLen.Timer @@ -434,11 +425,12 @@ def write_area(self, area: Areas, dbnumber: int, start: int, data: bytearray) -> wordlen = WordLen.Byte type_ = wordlen_to_ctypes[WordLen.Byte.value] size = len(data) - logger.debug(f"writing area: {area.name} dbnumber: {dbnumber} start: {start}: size {size}: " - f"wordlen {wordlen.name}={wordlen.value} type: {type_}") + logger.debug( + f"writing area: {area.name} dbnumber: {dbnumber} start: {start}: size {size}: " + f"wordlen {wordlen.name}={wordlen.value} type: {type_}" + ) cdata = (type_ * len(data)).from_buffer_copy(data) - return self._library.Cli_WriteArea(self._pointer, area.value, dbnumber, start, - size, wordlen.value, byref(cdata)) + return self._library.Cli_WriteArea(self._pointer, area.value, dbnumber, start, size, wordlen.value, byref(cdata)) def read_multi_vars(self, items) -> Tuple[int, S7DataItem]: """Reads different kind of variables from a PLC simultaneously. @@ -449,8 +441,7 @@ def read_multi_vars(self, items) -> Tuple[int, S7DataItem]: Returns: Tuple with the return code from the snap7 library and the list of items. """ - result = self._library.Cli_ReadMultiVars(self._pointer, byref(items), - c_int32(len(items))) + result = self._library.Cli_ReadMultiVars(self._pointer, byref(items), c_int32(len(items))) check_error(result, context="client") return result, items @@ -497,10 +488,7 @@ def list_blocks_of_type(self, blocktype: str, size: int) -> Union[int, Array]: data = (c_uint16 * size)() count = c_int(size) - result = self._library.Cli_ListBlocksOfType( - self._pointer, _blocktype, - byref(data), - byref(count)) + result = self._library.Cli_ListBlocksOfType(self._pointer, _blocktype, byref(data), byref(count)) logger.debug(f"number of items found: {count}") @@ -566,8 +554,7 @@ def set_session_password(self, password: str) -> int: """ if len(password) > 8: raise ValueError("Maximum password length is 8") - return self._library.Cli_SetSessionPassword(self._pointer, - c_char_p(password.encode())) + return self._library.Cli_SetSessionPassword(self._pointer, c_char_p(password.encode())) @error_wrap def clear_session_password(self) -> int: @@ -596,14 +583,12 @@ def set_connection_params(self, address: str, local_tsap: int, remote_tsap: int) """ if not re.match(ipv4, address): raise ValueError(f"{address} is invalid ipv4") - result = self._library.Cli_SetConnectionParams(self._pointer, address, - c_uint16(local_tsap), - c_uint16(remote_tsap)) + result = self._library.Cli_SetConnectionParams(self._pointer, address, c_uint16(local_tsap), c_uint16(remote_tsap)) if result != 0: raise ValueError("The parameter was invalid") def set_connection_type(self, connection_type: int): - """ Sets the connection resource type, i.e the way in which the Clients connects to a PLC. + """Sets the connection resource type, i.e the way in which the Clients connects to a PLC. Args: connection_type: 1 for PG, 2 for OP, 3 to 10 for S7 Basic @@ -612,8 +597,7 @@ def set_connection_type(self, connection_type: int): :obj:`ValueError`: if the result of setting the connection type is different than 0. """ - result = self._library.Cli_SetConnectionType(self._pointer, - c_uint16(connection_type)) + result = self._library.Cli_SetConnectionType(self._pointer, c_uint16(connection_type)) if result != 0: raise ValueError("The parameter was invalid") @@ -645,8 +629,7 @@ def ab_read(self, start: int, size: int) -> bytearray: type_ = wordlen_to_ctypes[wordlen.value] data = (type_ * size)() logger.debug(f"ab_read: start: {start}: size {size}: ") - result = self._library.Cli_ABRead(self._pointer, start, size, - byref(data)) + result = self._library.Cli_ABRead(self._pointer, start, size, byref(data)) check_error(result, context="client") return bytearray(data) @@ -665,8 +648,7 @@ def ab_write(self, start: int, data: bytearray) -> int: size = len(data) cdata = (type_ * size).from_buffer_copy(data) logger.debug(f"ab write: start: {start}: size: {size}: ") - return self._library.Cli_ABWrite( - self._pointer, start, size, byref(cdata)) + return self._library.Cli_ABWrite(self._pointer, start, size, byref(cdata)) def as_ab_read(self, start: int, size: int, data) -> int: """Reads a part of IPU area from a PLC asynchronously. @@ -680,8 +662,7 @@ def as_ab_read(self, start: int, size: int, data) -> int: Snap7 code. """ logger.debug(f"ab_read: start: {start}: size {size}: ") - result = self._library.Cli_AsABRead(self._pointer, start, size, - byref(data)) + result = self._library.Cli_AsABRead(self._pointer, start, size, byref(data)) check_error(result, context="client") return result @@ -700,13 +681,12 @@ def as_ab_write(self, start: int, data: bytearray) -> int: size = len(data) cdata = (type_ * size).from_buffer_copy(data) logger.debug(f"ab write: start: {start}: size: {size}: ") - result = self._library.Cli_AsABWrite( - self._pointer, start, size, byref(cdata)) + result = self._library.Cli_AsABWrite(self._pointer, start, size, byref(cdata)) check_error(result, context="client") return result def as_compress(self, time: int) -> int: - """ Performs the Compress action asynchronously. + """Performs the Compress action asynchronously. Args: time: timeout. @@ -930,12 +910,7 @@ def get_plc_datetime(self) -> datetime: check_error(result, context="client") return datetime( - year=buffer[5] + 1900, - month=buffer[4] + 1, - day=buffer[3], - hour=buffer[2], - minute=buffer[1], - second=buffer[0] + year=buffer[5] + 1900, month=buffer[4] + 1, day=buffer[3], hour=buffer[2], minute=buffer[1], second=buffer[0] ) @error_wrap @@ -975,7 +950,7 @@ def check_as_completion(self, p_value) -> int: def set_as_callback(self, pfn_clicompletion, p_usr): # Cli_SetAsCallback result = self._library.Cli_SetAsCallback(self._pointer, pfn_clicompletion, p_usr) - check_error(result, context='client') + check_error(result, context="client") return result def wait_as_completion(self, timeout: int) -> int: @@ -1023,7 +998,7 @@ def as_read_area(self, area: Areas, dbnumber: int, start: int, size: int, wordle logger.debug( f"reading area: {area.name} dbnumber: {dbnumber} start: {start} amount: {size} " f"wordlen: {wordlen.name}={wordlen.value}" - ) + ) result = self._library.Cli_AsReadArea(self._pointer, area.value, dbnumber, start, size, wordlen.value, pusrdata) check_error(result, context="client") return result @@ -1056,8 +1031,9 @@ def as_write_area(self, area: Areas, dbnumber: int, start: int, size: int, wordl Snap7 code. """ type_ = wordlen_to_ctypes[WordLen.Byte.value] - logger.debug(f"writing area: {area.name} dbnumber: {dbnumber} start: {start}: size {size}: " - f"wordlen {wordlen} type: {type_}") + logger.debug( + f"writing area: {area.name} dbnumber: {dbnumber} start: {start}: size {size}: " f"wordlen {wordlen} type: {type_}" + ) cdata = (type_ * len(pusrdata)).from_buffer_copy(pusrdata) res = self._library.Cli_AsWriteArea(self._pointer, area.value, dbnumber, start, size, wordlen.value, byref(cdata)) check_error(res, context="client") @@ -1244,7 +1220,7 @@ def as_upload(self, block_num: int, _buffer, size) -> int: Returns: Snap7 code. """ - block_type = block_types['DB'] + block_type = block_types["DB"] result = self._library.Cli_AsUpload(self._pointer, block_type, block_num, byref(_buffer), byref(size)) check_error(result, context="client") return result @@ -1356,7 +1332,7 @@ def error_text(self, error: int) -> str: text = create_string_buffer(buffer_size) response = self._library.Cli_ErrorText(error_code, byref(text), text_length) check_error(response) - result = bytearray(text)[:text_length.value].decode().strip('\x00') + result = bytearray(text)[: text_length.value].decode().strip("\x00") return result def get_cp_info(self) -> S7CpInfo: @@ -1443,7 +1419,7 @@ def iso_exchange_buffer(self, data: bytearray) -> bytearray: cdata = (c_byte * len(data)).from_buffer_copy(data) response = self._library.Cli_IsoExchangeBuffer(self._pointer, byref(cdata), byref(size)) check_error(response) - result = bytearray(cdata)[:size.value] + result = bytearray(cdata)[: size.value] return result def mb_read(self, start: int, size: int) -> bytearray: @@ -1505,7 +1481,7 @@ def read_szl_list(self) -> bytearray: items_count = c_int(sizeof(szl_list)) response = self._library.Cli_ReadSZLList(self._pointer, byref(szl_list), byref(items_count)) check_error(response, context="client") - result = bytearray(szl_list.List)[:items_count.value] + result = bytearray(szl_list.List)[: items_count.value] return result def set_plc_system_datetime(self) -> int: diff --git a/snap7/common.py b/snap7/common.py index 26b574c3..fc0f631d 100644 --- a/snap7/common.py +++ b/snap7/common.py @@ -7,7 +7,7 @@ from typing import Optional from ctypes.util import find_library -if platform.system() == 'Windows': +if platform.system() == "Windows": from ctypes import windll as cdll # type: ignore else: from ctypes import cdll @@ -18,14 +18,6 @@ ipv4 = r"^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$" -class ADict(dict): - """ - Accessing dict keys like an attribute. - """ - __getattr__ = dict.__getitem__ - __setattr__ = dict.__setitem__ # type: ignore - - class Snap7Library: """Snap7 loader and encapsulator. We make this a singleton to make sure the library is loaded only once. @@ -33,6 +25,7 @@ class Snap7Library: Attributes: lib_location: full path to the `snap7.dll` file. Optional. """ + _instance = None lib_location: Optional[str] @@ -44,7 +37,7 @@ def __new__(cls, *args, **kwargs): return cls._instance def __init__(self, lib_location: Optional[str] = None): - """ Loads the snap7 library using ctypes cdll. + """Loads the snap7 library using ctypes cdll. Args: lib_location: full path to the `snap7.dll` file. Optional. @@ -54,13 +47,25 @@ def __init__(self, lib_location: Optional[str] = None): """ if self.cdll: # type: ignore return - self.lib_location = (lib_location - or self.lib_location - or find_in_package() - or find_library('snap7') - or find_locally('snap7')) + self.lib_location = ( + lib_location or self.lib_location or find_in_package() or find_library("snap7") or find_locally("snap7") + ) if not self.lib_location: - raise RuntimeError("can't find snap7 library. If installed, try running ldconfig") + error = f"""can't find snap7 shared library. + +This probably means you are installing python-snap7 from source. When no binary wheel is found for you architecture, pip +install falls back on a source install. For this to work, you need to manually install the snap7 library, which python-snap7 +uses under the hood. + +The shortest path to success is to try to get a binary wheel working. Probably you are running on an unsupported +platform or python version. You are running: + +machine: {platform.machine()} +system: {platform.system()} +python version: {platform.python_version()} +""" + logger.error(error) + raise RuntimeError(error) self.cdll = cdll.LoadLibrary(self.lib_location) @@ -141,12 +146,12 @@ def find_in_package() -> Optional[str]: """ basedir = pathlib.Path(__file__).parent.absolute() if sys.platform == "darwin": - lib = 'libsnap7.dylib' + lib = "libsnap7.dylib" elif sys.platform == "win32": - lib = 'snap7.dll' + lib = "snap7.dll" else: - lib = 'libsnap7.so' - full_path = basedir.joinpath('lib', lib) + lib = "libsnap7.so" + full_path = basedir.joinpath("lib", lib) if Path.exists(full_path) and Path.is_file(full_path): return str(full_path) return None diff --git a/snap7/error.py b/snap7/error.py index 637b0481..24ea573e 100644 --- a/snap7/error.py +++ b/snap7/error.py @@ -7,92 +7,92 @@ """ s7_client_errors = { - 0x00100000: 'errNegotiatingPDU', - 0x00200000: 'errCliInvalidParams', - 0x00300000: 'errCliJobPending', - 0x00400000: 'errCliTooManyItems', - 0x00500000: 'errCliInvalidWordLen', - 0x00600000: 'errCliPartialDataWritten', - 0x00700000: 'errCliSizeOverPDU', - 0x00800000: 'errCliInvalidPlcAnswer', - 0x00900000: 'errCliAddressOutOfRange', - 0x00A00000: 'errCliInvalidTransportSize', - 0x00B00000: 'errCliWriteDataSizeMismatch', - 0x00C00000: 'errCliItemNotAvailable', - 0x00D00000: 'errCliInvalidValue', - 0x00E00000: 'errCliCannotStartPLC', - 0x00F00000: 'errCliAlreadyRun', - 0x01000000: 'errCliCannotStopPLC', - 0x01100000: 'errCliCannotCopyRamToRom', - 0x01200000: 'errCliCannotCompress', - 0x01300000: 'errCliAlreadyStop', - 0x01400000: 'errCliFunNotAvailable', - 0x01500000: 'errCliUploadSequenceFailed', - 0x01600000: 'errCliInvalidDataSizeRecvd', - 0x01700000: 'errCliInvalidBlockType', - 0x01800000: 'errCliInvalidBlockNumber', - 0x01900000: 'errCliInvalidBlockSize', - 0x01A00000: 'errCliDownloadSequenceFailed', - 0x01B00000: 'errCliInsertRefused', - 0x01C00000: 'errCliDeleteRefused', - 0x01D00000: 'errCliNeedPassword', - 0x01E00000: 'errCliInvalidPassword', - 0x01F00000: 'errCliNoPasswordToSetOrClear', - 0x02000000: 'errCliJobTimeout', - 0x02100000: 'errCliPartialDataRead', - 0x02200000: 'errCliBufferTooSmall', - 0x02300000: 'errCliFunctionRefused', - 0x02400000: 'errCliDestroying', - 0x02500000: 'errCliInvalidParamNumber', - 0x02600000: 'errCliCannotChangeParam', + 0x00100000: "errNegotiatingPDU", + 0x00200000: "errCliInvalidParams", + 0x00300000: "errCliJobPending", + 0x00400000: "errCliTooManyItems", + 0x00500000: "errCliInvalidWordLen", + 0x00600000: "errCliPartialDataWritten", + 0x00700000: "errCliSizeOverPDU", + 0x00800000: "errCliInvalidPlcAnswer", + 0x00900000: "errCliAddressOutOfRange", + 0x00A00000: "errCliInvalidTransportSize", + 0x00B00000: "errCliWriteDataSizeMismatch", + 0x00C00000: "errCliItemNotAvailable", + 0x00D00000: "errCliInvalidValue", + 0x00E00000: "errCliCannotStartPLC", + 0x00F00000: "errCliAlreadyRun", + 0x01000000: "errCliCannotStopPLC", + 0x01100000: "errCliCannotCopyRamToRom", + 0x01200000: "errCliCannotCompress", + 0x01300000: "errCliAlreadyStop", + 0x01400000: "errCliFunNotAvailable", + 0x01500000: "errCliUploadSequenceFailed", + 0x01600000: "errCliInvalidDataSizeRecvd", + 0x01700000: "errCliInvalidBlockType", + 0x01800000: "errCliInvalidBlockNumber", + 0x01900000: "errCliInvalidBlockSize", + 0x01A00000: "errCliDownloadSequenceFailed", + 0x01B00000: "errCliInsertRefused", + 0x01C00000: "errCliDeleteRefused", + 0x01D00000: "errCliNeedPassword", + 0x01E00000: "errCliInvalidPassword", + 0x01F00000: "errCliNoPasswordToSetOrClear", + 0x02000000: "errCliJobTimeout", + 0x02100000: "errCliPartialDataRead", + 0x02200000: "errCliBufferTooSmall", + 0x02300000: "errCliFunctionRefused", + 0x02400000: "errCliDestroying", + 0x02500000: "errCliInvalidParamNumber", + 0x02600000: "errCliCannotChangeParam", } isotcp_errors = { - 0x00010000: 'errIsoConnect', - 0x00020000: 'errIsoDisconnect', - 0x00030000: 'errIsoInvalidPDU', - 0x00040000: 'errIsoInvalidDataSize', - 0x00050000: 'errIsoNullPointer', - 0x00060000: 'errIsoShortPacket', - 0x00070000: 'errIsoTooManyFragments', - 0x00080000: 'errIsoPduOverflow', - 0x00090000: 'errIsoSendPacket', - 0x000A0000: 'errIsoRecvPacket', - 0x000B0000: 'errIsoInvalidParams', - 0x000C0000: 'errIsoResvd_1', - 0x000D0000: 'errIsoResvd_2', - 0x000E0000: 'errIsoResvd_3', - 0x000F0000: 'errIsoResvd_4', + 0x00010000: "errIsoConnect", + 0x00020000: "errIsoDisconnect", + 0x00030000: "errIsoInvalidPDU", + 0x00040000: "errIsoInvalidDataSize", + 0x00050000: "errIsoNullPointer", + 0x00060000: "errIsoShortPacket", + 0x00070000: "errIsoTooManyFragments", + 0x00080000: "errIsoPduOverflow", + 0x00090000: "errIsoSendPacket", + 0x000A0000: "errIsoRecvPacket", + 0x000B0000: "errIsoInvalidParams", + 0x000C0000: "errIsoResvd_1", + 0x000D0000: "errIsoResvd_2", + 0x000E0000: "errIsoResvd_3", + 0x000F0000: "errIsoResvd_4", } tcp_errors = { - 0x00000001: 'evcServerStarted', - 0x00000002: 'evcServerStopped', - 0x00000004: 'evcListenerCannotStart', - 0x00000008: 'evcClientAdded', - 0x00000010: 'evcClientRejected', - 0x00000020: 'evcClientNoRoom', - 0x00000040: 'evcClientException', - 0x00000080: 'evcClientDisconnected', - 0x00000100: 'evcClientTerminated', - 0x00000200: 'evcClientsDropped', - 0x00000400: 'evcReserved_00000400', - 0x00000800: 'evcReserved_00000800', - 0x00001000: 'evcReserved_00001000', - 0x00002000: 'evcReserved_00002000', - 0x00004000: 'evcReserved_00004000', - 0x00008000: 'evcReserved_00008000', + 0x00000001: "evcServerStarted", + 0x00000002: "evcServerStopped", + 0x00000004: "evcListenerCannotStart", + 0x00000008: "evcClientAdded", + 0x00000010: "evcClientRejected", + 0x00000020: "evcClientNoRoom", + 0x00000040: "evcClientException", + 0x00000080: "evcClientDisconnected", + 0x00000100: "evcClientTerminated", + 0x00000200: "evcClientsDropped", + 0x00000400: "evcReserved_00000400", + 0x00000800: "evcReserved_00000800", + 0x00001000: "evcReserved_00001000", + 0x00002000: "evcReserved_00002000", + 0x00004000: "evcReserved_00004000", + 0x00008000: "evcReserved_00008000", } s7_server_errors = { - 0x00100000: 'errSrvCannotStart', - 0x00200000: 'errSrvDBNullPointer', - 0x00300000: 'errSrvAreaAlreadyExists', - 0x00400000: 'errSrvUnknownArea', - 0x00500000: 'verrSrvInvalidParams', - 0x00600000: 'errSrvTooManyDB', - 0x00700000: 'errSrvInvalidParamNumber', - 0x00800000: 'errSrvCannotChangeParam', + 0x00100000: "errSrvCannotStart", + 0x00200000: "errSrvDBNullPointer", + 0x00300000: "errSrvAreaAlreadyExists", + 0x00400000: "errSrvUnknownArea", + 0x00500000: "verrSrvInvalidParams", + 0x00600000: "errSrvTooManyDB", + 0x00700000: "errSrvInvalidParamNumber", + 0x00800000: "errSrvCannotChangeParam", } client_errors = s7_client_errors.copy() diff --git a/snap7/logo.py b/snap7/logo.py index 82b74d14..ff348cf4 100644 --- a/snap7/logo.py +++ b/snap7/logo.py @@ -1,6 +1,7 @@ """ Snap7 client used for connection to a siemens LOGO 7/8 server. """ + import re import struct import logging @@ -134,8 +135,7 @@ def read(self, vm_address: str): logger.debug(f"start:{start}, wordlen:{wordlen.name}={wordlen.value}, data-length:{len(data)}") - result = self.library.Cli_ReadArea(self.pointer, area.value, db_number, start, - size, wordlen.value, byref(data)) + result = self.library.Cli_ReadArea(self.pointer, area.value, db_number, start, size, wordlen.value, byref(data)) check_error(result, context="client") # transform result to int value if wordlen == WordLen.Bit: @@ -227,9 +227,7 @@ def db_read(self, db_number: int, start: int, size: int) -> bytearray: type_ = wordlen_to_ctypes[WordLen.Byte.value] data = (type_ * size)() - result = (self.library.Cli_DBRead( - self.pointer, db_number, start, size, - byref(data))) + result = self.library.Cli_DBRead(self.pointer, db_number, start, size, byref(data)) check_error(result, context="client") return bytearray(data) @@ -270,9 +268,9 @@ def set_connection_params(self, ip_address: str, tsap_snap7: int, tsap_logo: int """ if not re.match(ipv4, ip_address): raise ValueError(f"{ip_address} is invalid ipv4") - result = self.library.Cli_SetConnectionParams(self.pointer, ip_address.encode(), - c_uint16(tsap_snap7), - c_uint16(tsap_logo)) + result = self.library.Cli_SetConnectionParams( + self.pointer, ip_address.encode(), c_uint16(tsap_snap7), c_uint16(tsap_logo) + ) if result != 0: raise ValueError("The parameter was invalid") @@ -286,8 +284,7 @@ def set_connection_type(self, connection_type: int): Raises: :obj:`ValueError`: if the snap7 error code is diferent from 0. """ - result = self.library.Cli_SetConnectionType(self.pointer, - c_uint16(connection_type)) + result = self.library.Cli_SetConnectionType(self.pointer, c_uint16(connection_type)) if result != 0: raise ValueError("The parameter was invalid") @@ -334,7 +331,6 @@ def get_param(self, number) -> int: logger.debug(f"retreiving param number {number}") type_ = param_types[number] value = type_() - code = self.library.Cli_GetParam(self.pointer, c_int(number), - byref(value)) + code = self.library.Cli_GetParam(self.pointer, c_int(number), byref(value)) check_error(code) return value.value diff --git a/snap7/partner.py b/snap7/partner.py index 7f3b48ef..df05481b 100644 --- a/snap7/partner.py +++ b/snap7/partner.py @@ -7,6 +7,7 @@ can send data asynchronously. The only difference between them is the one who is requesting the connection. """ + import re import logging from ctypes import byref, c_int, c_int32, c_uint32, c_void_p @@ -32,6 +33,7 @@ class Partner: """ A snap7 partner. """ + _pointer: Optional[c_void_p] def __init__(self, active: bool = False): @@ -126,8 +128,7 @@ def get_param(self, number) -> int: logger.debug(f"retreiving param number {number}") type_ = param_types[number] value = type_() - code = self._library.Par_GetParam(self._pointer, c_int(number), - byref(value)) + code = self._library.Par_GetParam(self._pointer, c_int(number), byref(value)) check_error(code) return value.value @@ -141,10 +142,7 @@ def get_stats(self) -> Tuple[c_uint32, c_uint32, c_uint32, c_uint32]: recv = c_uint32() send_errors = c_uint32() recv_errors = c_uint32() - result = self._library.Par_GetStats(self._pointer, byref(sent), - byref(recv), - byref(send_errors), - byref(recv_errors)) + result = self._library.Par_GetStats(self._pointer, byref(sent), byref(recv), byref(send_errors), byref(recv_errors)) check_error(result, "partner") return sent, recv, send_errors, recv_errors @@ -169,11 +167,9 @@ def get_times(self) -> Tuple[c_int32, c_int32]: @error_wrap def set_param(self, number: int, value) -> int: - """Sets an internal Partner object parameter. - """ + """Sets an internal Partner object parameter.""" logger.debug(f"setting param number {number} to {value}") - return self._library.Par_SetParam(self._pointer, number, - byref(c_int(value))) + return self._library.Par_SetParam(self._pointer, number, byref(c_int(value))) def set_recv_callback(self) -> int: """ @@ -214,9 +210,9 @@ def start_to(self, local_ip: str, remote_ip: str, local_tsap: int, remote_tsap: if not re.match(ipv4, remote_ip): raise ValueError(f"{remote_ip} is invalid ipv4") logger.info(f"starting partnering from {local_ip} to {remote_ip}") - return self._library.Par_StartTo(self._pointer, local_ip.encode(), remote_ip.encode(), - word(local_tsap), - word(remote_tsap)) + return self._library.Par_StartTo( + self._pointer, local_ip.encode(), remote_ip.encode(), word(local_tsap), word(remote_tsap) + ) def stop(self) -> int: """ diff --git a/snap7/server/__init__.py b/snap7/server/__init__.py index 42c48c30..2f6ada2a 100644 --- a/snap7/server/__init__.py +++ b/snap7/server/__init__.py @@ -1,6 +1,7 @@ """ Snap7 server used for mimicking a siemens 7 server. """ + import re import time import ctypes @@ -18,6 +19,7 @@ def error_wrap(func): """Parses a s7 error code returned the decorated function.""" + def f(*args, **kw): code = func(*args, **kw) check_error(code, context="server") @@ -61,20 +63,18 @@ def event_text(self, event: SrvEvent) -> str: len_ = 1024 text_type = ctypes.c_char * len_ text = text_type() - error = self.library.Srv_EventText(ctypes.byref(event), - ctypes.byref(text), len_) + error = self.library.Srv_EventText(ctypes.byref(event), ctypes.byref(text), len_) check_error(error) - return text.value.decode('ascii') + return text.value.decode("ascii") def create(self): - """Create the server. - """ + """Create the server.""" logger.info("creating server") self.library.Srv_Create.restype = S7Object self.pointer = S7Object(self.library.Srv_Create()) @error_wrap - def register_area(self, area_code: int, index: int, userdata): + def register_area(self, area_code: int, index: int, userdata: ctypes.Array[ctypes.c_int8]): """Shares a memory area with the server. That memory block will be visible by the clients. @@ -93,7 +93,7 @@ def register_area(self, area_code: int, index: int, userdata): @error_wrap def set_events_callback(self, call_back: Callable[..., Any]) -> int: """Sets the user callback that the Server object has to call when an - event is created. + event is created. """ logger.info("setting event callback") callback_wrap: Callable[..., Any] = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(SrvEvent), ctypes.c_int) @@ -126,9 +126,7 @@ def set_read_events_callback(self, call_back: Callable[..., Any]): call_back: a callback function that accepts a pevent argument. """ logger.info("setting read event callback") - callback_wrapper: Callable[..., Any] = ctypes.CFUNCTYPE(None, ctypes.c_void_p, - ctypes.POINTER(SrvEvent), - ctypes.c_int) + callback_wrapper: Callable[..., Any] = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(SrvEvent), ctypes.c_int) def wrapper(usrptr: Optional[ctypes.c_void_p], pevent: SrvEvent, size: int) -> int: """Wraps python function into a ctypes function @@ -146,8 +144,7 @@ def wrapper(usrptr: Optional[ctypes.c_void_p], pevent: SrvEvent, size: int) -> i return 0 self._read_callback = callback_wrapper(wrapper) - return self.library.Srv_SetReadEventsCallback(self.pointer, - self._read_callback) + return self.library.Srv_SetReadEventsCallback(self.pointer, self._read_callback) def _set_log_callback(self): """Sets a callback that logs the events""" @@ -180,7 +177,7 @@ def stop(self): def destroy(self): """Destroy the server.""" logger.info("destroying server") - if self.library: + if hasattr(self, "library") and self.library: self.library.Srv_Destroy(ctypes.byref(self.pointer)) def get_status(self) -> Tuple[str, str, int]: @@ -194,16 +191,12 @@ def get_status(self) -> Tuple[str, str, int]: server_status = ctypes.c_int() cpu_status = ctypes.c_int() clients_count = ctypes.c_int() - error = self.library.Srv_GetStatus(self.pointer, ctypes.byref(server_status), - ctypes.byref(cpu_status), - ctypes.byref(clients_count)) + error = self.library.Srv_GetStatus( + self.pointer, ctypes.byref(server_status), ctypes.byref(cpu_status), ctypes.byref(clients_count) + ) check_error(error) logger.debug(f"status server {server_status.value} cpu {cpu_status.value} clients {clients_count.value}") - return ( - server_statuses[server_status.value], - cpu_statuses[cpu_status.value], - clients_count.value - ) + return (server_statuses[server_status.value], cpu_statuses[cpu_status.value], clients_count.value) @error_wrap def unregister_area(self, area_code: int, index: int): @@ -280,8 +273,7 @@ def set_param(self, number: int, value: int): Error code from snap7 library. """ logger.debug(f"setting param number {number} to {value}") - return self.library.Srv_SetParam(self.pointer, number, - ctypes.byref(ctypes.c_int(value))) + return self.library.Srv_SetParam(self.pointer, number, ctypes.byref(ctypes.c_int(value))) @error_wrap def set_mask(self, kind: int, mask: int): @@ -324,8 +316,7 @@ def pick_event(self) -> Optional[SrvEvent]: logger.debug("checking event queue") event = SrvEvent() ready = ctypes.c_int32() - code = self.library.Srv_PickEvent(self.pointer, ctypes.byref(event), - ctypes.byref(ready)) + code = self.library.Srv_PickEvent(self.pointer, ctypes.byref(event), ctypes.byref(ready)) check_error(code) if ready: logger.debug(f"one event ready: {event}") @@ -344,8 +335,7 @@ def get_param(self, number) -> int: """ logger.debug(f"retreiving param number {number}") value = ctypes.c_int() - code = self.library.Srv_GetParam(self.pointer, number, - ctypes.byref(value)) + code = self.library.Srv_GetParam(self.pointer, number, ctypes.byref(value)) check_error(code) return value.value @@ -396,9 +386,8 @@ def mainloop(tcpport: int = 1102, init_standard_values: bool = False): if init_standard_values: ba = _init_standard_values() - DBdata = wordlen_to_ctypes[WordLen.Byte.value] * len(ba) - DBdata = DBdata.from_buffer(ba) - server.register_area(srvAreaDB, 0, DBdata) + userdata = wordlen_to_ctypes[WordLen.Byte.value] * len(ba) + server.register_area(srvAreaDB, 0, userdata.from_buffer(ba)) server.start(tcpport=tcpport) while True: @@ -412,7 +401,7 @@ def mainloop(tcpport: int = 1102, init_standard_values: bool = False): def _init_standard_values() -> bytearray: - ''' Standard values + """Standard values * Boolean BYTE BIT VALUE 0 0 True @@ -472,55 +461,55 @@ def _init_standard_values() -> bytearray: BYTE VALUE 400 \x00\x00 404 \x12\x34 - 408 \xAB\xCD - 412 \xFF\xFF + 408 \xab\xcd + 412 \xff\xff * Double Word BYTE VALUE 500 \x00\x00\x00\x00 508 \x12\x34\x56\x78 - 516 \x12\x34\xAB\xCD - 524 \xFF\xFF\xFF\xFF - ''' + 516 \x12\x34\xab\xcd + 524 \xff\xff\xff\xff + """ ba = bytearray(1000) # 1. Bool 1 byte ba[0] = 0b10101010 # 2. Small int 1 byte - ba[10:10 + 1] = struct.pack(">b", -128) - ba[11:11 + 1] = struct.pack(">b", 0) - ba[12:12 + 1] = struct.pack(">b", 100) - ba[13:13 + 1] = struct.pack(">b", 127) + ba[10 : 10 + 1] = struct.pack(">b", -128) + ba[11 : 11 + 1] = struct.pack(">b", 0) + ba[12 : 12 + 1] = struct.pack(">b", 100) + ba[13 : 13 + 1] = struct.pack(">b", 127) # 3. Unsigned small int 1 byte - ba[20:20 + 1] = struct.pack("B", 0) - ba[21:21 + 1] = struct.pack("B", 255) + ba[20 : 20 + 1] = struct.pack("B", 0) + ba[21 : 21 + 1] = struct.pack("B", 255) # 4. Int 2 bytes - ba[30:30 + 2] = struct.pack(">h", -32768) - ba[32:32 + 2] = struct.pack(">h", -1234) - ba[34:34 + 2] = struct.pack(">h", 0) - ba[36:36 + 2] = struct.pack(">h", 1234) - ba[38:38 + 2] = struct.pack(">h", 32767) + ba[30 : 30 + 2] = struct.pack(">h", -32768) + ba[32 : 32 + 2] = struct.pack(">h", -1234) + ba[34 : 34 + 2] = struct.pack(">h", 0) + ba[36 : 36 + 2] = struct.pack(">h", 1234) + ba[38 : 38 + 2] = struct.pack(">h", 32767) # 5. DInt 4 bytes - ba[40:40 + 4] = struct.pack(">i", -2147483648) - ba[44:44 + 4] = struct.pack(">i", -32768) - ba[48:48 + 4] = struct.pack(">i", 0) - ba[52:52 + 4] = struct.pack(">i", 32767) - ba[56:56 + 4] = struct.pack(">i", 2147483647) + ba[40 : 40 + 4] = struct.pack(">i", -2147483648) + ba[44 : 44 + 4] = struct.pack(">i", -32768) + ba[48 : 48 + 4] = struct.pack(">i", 0) + ba[52 : 52 + 4] = struct.pack(">i", 32767) + ba[56 : 56 + 4] = struct.pack(">i", 2147483647) # 6. Real 4 bytes - ba[60:60 + 4] = struct.pack(">f", -3.402823e38) - ba[64:64 + 4] = struct.pack(">f", -3.402823e12) - ba[68:68 + 4] = struct.pack(">f", -175494351e-38) - ba[72:72 + 4] = struct.pack(">f", -1.175494351e-12) - ba[76:76 + 4] = struct.pack(">f", 0.0) - ba[80:80 + 4] = struct.pack(">f", 1.175494351e-38) - ba[84:84 + 4] = struct.pack(">f", 1.175494351e-12) - ba[88:88 + 4] = struct.pack(">f", 3.402823466e12) - ba[92:92 + 4] = struct.pack(">f", 3.402823466e38) + ba[60 : 60 + 4] = struct.pack(">f", -3.402823e38) + ba[64 : 64 + 4] = struct.pack(">f", -3.402823e12) + ba[68 : 68 + 4] = struct.pack(">f", -175494351e-38) + ba[72 : 72 + 4] = struct.pack(">f", -1.175494351e-12) + ba[76 : 76 + 4] = struct.pack(">f", 0.0) + ba[80 : 80 + 4] = struct.pack(">f", 1.175494351e-38) + ba[84 : 84 + 4] = struct.pack(">f", 1.175494351e-12) + ba[88 : 88 + 4] = struct.pack(">f", 3.402823466e12) + ba[92 : 92 + 4] = struct.pack(">f", 3.402823466e38) # 7. String 1 byte per char string = "the brown fox jumps over the lazy dog" # len = 37 @@ -530,15 +519,15 @@ def _init_standard_values() -> bytearray: ba[i] = ord(letter) # 8. WORD 4 bytes - ba[400:400 + 4] = b"\x00\x00" - ba[404:404 + 4] = b"\x12\x34" - ba[408:408 + 4] = b"\xAB\xCD" - ba[412:412 + 4] = b"\xFF\xFF" + ba[400 : 400 + 4] = b"\x00\x00" + ba[404 : 404 + 4] = b"\x12\x34" + ba[408 : 408 + 4] = b"\xab\xcd" + ba[412 : 412 + 4] = b"\xff\xff" # # 9 DWORD 8 bytes - ba[500:500 + 8] = b"\x00\x00\x00\x00" - ba[508:508 + 8] = b"\x12\x34\x56\x78" - ba[516:516 + 8] = b"\x12\x34\xAB\xCD" - ba[524:524 + 8] = b"\xFF\xFF\xFF\xFF" + ba[500 : 500 + 8] = b"\x00\x00\x00\x00" + ba[508 : 508 + 8] = b"\x12\x34\x56\x78" + ba[516 : 516 + 8] = b"\x12\x34\xab\xcd" + ba[524 : 524 + 8] = b"\xff\xff\xff\xff" return ba diff --git a/snap7/server/__main__.py b/snap7/server/__main__.py index 38734536..015bddc1 100644 --- a/snap7/server/__main__.py +++ b/snap7/server/__main__.py @@ -9,10 +9,9 @@ try: import click -except ImportError as e: - print(e) +except ImportError: print("Try using 'pip install python-snap7[cli]'") - exit() + raise from snap7 import __version__ from snap7.common import load_library diff --git a/snap7/types.py b/snap7/types.py index 19737e76..fb1375c3 100755 --- a/snap7/types.py +++ b/snap7/types.py @@ -1,11 +1,10 @@ """ Python equivalent for snap7 specific types. """ + import ctypes from enum import Enum -from .common import ADict - S7Object = ctypes.c_void_p buffer_size = 65536 buffer_type = ctypes.c_ubyte * buffer_size @@ -30,7 +29,7 @@ RecoveryTime = 14 KeepAliveTime = 15 -param_types = ADict({ +param_types = { LocalPort: ctypes.c_uint16, RemotePort: ctypes.c_uint16, PingTimeout: ctypes.c_int32, @@ -46,7 +45,7 @@ BRecvTimeout: ctypes.c_int32, RecoveryTime: ctypes.c_uint32, KeepAliveTime: ctypes.c_uint32, -}) +} # mask types mkEvent = 0 @@ -71,15 +70,14 @@ class Areas(Enum): S7AreaCT = 0x1C S7AreaTM = 0x1D - -areas = ADict({ - 'PE': 0x81, - 'PA': 0x82, - 'MK': 0x83, - 'DB': 0x84, - 'CT': 0x1C, - 'TM': 0x1D, -}) +areas = { + "PE": S7AreaPE, + "PA": S7AreaPA, + "MK": S7AreaMK, + "DB": S7AreaDB, + "CT": S7AreaCT, + "TM": S7AreaTM, +} # Word Length @@ -117,16 +115,16 @@ class WordLen(Enum): srvAreaTM = 4 srvAreaDB = 5 -server_areas = ADict({ - 'PE': 0, - 'PA': 1, - 'MK': 2, - 'CT': 3, - 'TM': 4, - 'DB': 5, -}) +server_areas = { + "PE": srvAreaPE, + "PA": srvAreaPA, + "MK": srvAreaMK, + "CT": srvAreaCT, + "TM": srvAreaTM, + "DB": srvAreaDB, +} -wordlen_to_ctypes = ADict({ +wordlen_to_ctypes = { S7WLBit: ctypes.c_int16, S7WLByte: ctypes.c_int8, S7WLWord: ctypes.c_int16, @@ -134,82 +132,86 @@ class WordLen(Enum): S7WLReal: ctypes.c_int32, S7WLCounter: ctypes.c_int16, S7WLTimer: ctypes.c_int16, -}) - -block_types = ADict({ - 'OB': ctypes.c_int(0x38), - 'DB': ctypes.c_int(0x41), - 'SDB': ctypes.c_int(0x42), - 'FC': ctypes.c_int(0x43), - 'SFC': ctypes.c_int(0x44), - 'FB': ctypes.c_int(0x45), - 'SFB': ctypes.c_int(0x46), -}) +} + +block_types = { + "OB": ctypes.c_int(0x38), + "DB": ctypes.c_int(0x41), + "SDB": ctypes.c_int(0x42), + "FC": ctypes.c_int(0x43), + "SFC": ctypes.c_int(0x44), + "FB": ctypes.c_int(0x45), + "SFB": ctypes.c_int(0x46), +} server_statuses = { - 0: 'SrvStopped', - 1: 'SrvRunning', - 2: 'SrvError', + 0: "SrvStopped", + 1: "SrvRunning", + 2: "SrvError", } cpu_statuses = { - 0: 'S7CpuStatusUnknown', - 4: 'S7CpuStatusStop', - 8: 'S7CpuStatusRun', + 0: "S7CpuStatusUnknown", + 4: "S7CpuStatusStop", + 8: "S7CpuStatusRun", } class SrvEvent(ctypes.Structure): _fields_ = [ - ('EvtTime', time_t), - ('EvtSender', ctypes.c_int), - ('EvtCode', longword), - ('EvtRetCode', word), - ('EvtParam1', word), - ('EvtParam2', word), - ('EvtParam3', word), - ('EvtParam4', word), + ("EvtTime", time_t), + ("EvtSender", ctypes.c_int), + ("EvtCode", longword), + ("EvtRetCode", word), + ("EvtParam1", word), + ("EvtParam2", word), + ("EvtParam3", word), + ("EvtParam4", word), ] def __str__(self) -> str: - return f"" + return ( + f"" + ) class BlocksList(ctypes.Structure): _fields_ = [ - ('OBCount', ctypes.c_int32), - ('FBCount', ctypes.c_int32), - ('FCCount', ctypes.c_int32), - ('SFBCount', ctypes.c_int32), - ('SFCCount', ctypes.c_int32), - ('DBCount', ctypes.c_int32), - ('SDBCount', ctypes.c_int32), + ("OBCount", ctypes.c_int32), + ("FBCount", ctypes.c_int32), + ("FCCount", ctypes.c_int32), + ("SFBCount", ctypes.c_int32), + ("SFCCount", ctypes.c_int32), + ("DBCount", ctypes.c_int32), + ("SDBCount", ctypes.c_int32), ] def __str__(self) -> str: - return f"" + return ( + f"" + ) class TS7BlockInfo(ctypes.Structure): _fields_ = [ - ('BlkType', ctypes.c_int32), - ('BlkNumber', ctypes.c_int32), - ('BlkLang', ctypes.c_int32), - ('BlkFlags', ctypes.c_int32), - ('MC7Size', ctypes.c_int32), - ('LoadSize', ctypes.c_int32), - ('LocalData', ctypes.c_int32), - ('SBBLength', ctypes.c_int32), - ('CheckSum', ctypes.c_int32), - ('Version', ctypes.c_int32), - ('CodeDate', ctypes.c_char * 11), - ('IntfDate', ctypes.c_char * 11), - ('Author', ctypes.c_char * 9), - ('Family', ctypes.c_char * 9), - ('Header', ctypes.c_char * 9), + ("BlkType", ctypes.c_int32), + ("BlkNumber", ctypes.c_int32), + ("BlkLang", ctypes.c_int32), + ("BlkFlags", ctypes.c_int32), + ("MC7Size", ctypes.c_int32), + ("LoadSize", ctypes.c_int32), + ("LocalData", ctypes.c_int32), + ("SBBLength", ctypes.c_int32), + ("CheckSum", ctypes.c_int32), + ("Version", ctypes.c_int32), + ("CodeDate", ctypes.c_char * 11), + ("IntfDate", ctypes.c_char * 11), + ("Author", ctypes.c_char * 9), + ("Family", ctypes.c_char * 9), + ("Header", ctypes.c_char * 9), ] def __str__(self) -> str: @@ -234,43 +236,45 @@ def __str__(self) -> str: class S7DataItem(ctypes.Structure): _pack_ = 1 _fields_ = [ - ('Area', ctypes.c_int32), - ('WordLen', ctypes.c_int32), - ('Result', ctypes.c_int32), - ('DBNumber', ctypes.c_int32), - ('Start', ctypes.c_int32), - ('Amount', ctypes.c_int32), - ('pData', ctypes.POINTER(ctypes.c_uint8)) + ("Area", ctypes.c_int32), + ("WordLen", ctypes.c_int32), + ("Result", ctypes.c_int32), + ("DBNumber", ctypes.c_int32), + ("Start", ctypes.c_int32), + ("Amount", ctypes.c_int32), + ("pData", ctypes.POINTER(ctypes.c_uint8)), ] def __str__(self) -> str: - return f"" + return ( + f"" + ) class S7CpuInfo(ctypes.Structure): _fields_ = [ - ('ModuleTypeName', ctypes.c_char * 33), - ('SerialNumber', ctypes.c_char * 25), - ('ASName', ctypes.c_char * 25), - ('Copyright', ctypes.c_char * 27), - ('ModuleName', ctypes.c_char * 25) + ("ModuleTypeName", ctypes.c_char * 33), + ("SerialNumber", ctypes.c_char * 25), + ("ASName", ctypes.c_char * 25), + ("Copyright", ctypes.c_char * 27), + ("ModuleName", ctypes.c_char * 25), ] def __str__(self): - return f"" + return ( + f"" + ) class S7SZLHeader(ctypes.Structure): """ - LengthDR: Length of a data record of the partial list in bytes - NDR: Number of data records contained in the partial list + LengthDR: Length of a data record of the partial list in bytes + NDR: Number of data records contained in the partial list """ - _fields_ = [ - ('LengthDR', ctypes.c_uint16), - ('NDR', ctypes.c_uint16) - ] + + _fields_ = [("LengthDR", ctypes.c_uint16), ("NDR", ctypes.c_uint16)] def __str__(self) -> str: return f"" @@ -278,50 +282,43 @@ def __str__(self) -> str: class S7SZL(ctypes.Structure): """See §33.1 of System Software for S7-300/400 System and Standard Functions""" - _fields_ = [ - ('Header', S7SZLHeader), - ('Data', ctypes.c_byte * (0x4000 - 4)) - ] + + _fields_ = [("Header", S7SZLHeader), ("Data", ctypes.c_byte * (0x4000 - 4))] def __str__(self) -> str: return f"" class S7SZLList(ctypes.Structure): - _fields_ = [ - ('Header', S7SZLHeader), - ('List', word * (0x4000 - 2)) - ] + _fields_ = [("Header", S7SZLHeader), ("List", word * (0x4000 - 2))] class S7OrderCode(ctypes.Structure): - _fields_ = [ - ('OrderCode', ctypes.c_char * 21), - ('V1', ctypes.c_byte), - ('V2', ctypes.c_byte), - ('V3', ctypes.c_byte) - ] + _fields_ = [("OrderCode", ctypes.c_char * 21), ("V1", ctypes.c_byte), ("V2", ctypes.c_byte), ("V3", ctypes.c_byte)] class S7CpInfo(ctypes.Structure): _fields_ = [ - ('MaxPduLength', ctypes.c_uint16), - ('MaxConnections', ctypes.c_uint16), - ('MaxMpiRate', ctypes.c_uint16), - ('MaxBusRate', ctypes.c_uint16) + ("MaxPduLength", ctypes.c_uint16), + ("MaxConnections", ctypes.c_uint16), + ("MaxMpiRate", ctypes.c_uint16), + ("MaxBusRate", ctypes.c_uint16), ] def __str__(self) -> str: - return f"" + return ( + f"" + ) class S7Protection(ctypes.Structure): """See §33.19 of System Software for S7-300/400 System and Standard Functions""" + _fields_ = [ - ('sch_schal', word), - ('sch_par', word), - ('sch_rel', word), - ('bart_sch', word), - ('anl_sch', word), + ("sch_schal", word), + ("sch_par", word), + ("sch_rel", word), + ("bart_sch", word), + ("anl_sch", word), ] diff --git a/snap7/util.py b/snap7/util.py deleted file mode 100644 index cfa8082d..00000000 --- a/snap7/util.py +++ /dev/null @@ -1,1880 +0,0 @@ -""" -This module contains utility functions for working with PLC DB objects. -There are functions to work with the raw bytearray data snap7 functions return -In order to work with this data you need to make python able to work with the -PLC bytearray data. - -For example code see test_util.py and example.py in the example folder. - - -example:: - - spec/DB layout - - # Byte index Variable name Datatype - layout=\"\"\" - 4 ID INT - 6 NAME STRING[6] - - 12.0 testbool1 BOOL - 12.1 testbool2 BOOL - 12.2 testbool3 BOOL - 12.3 testbool4 BOOL - 12.4 testbool5 BOOL - 12.5 testbool6 BOOL - 12.6 testbool7 BOOL - 12.7 testbool8 BOOL - 13 testReal REAL - 17 testDword DWORD - \"\"\" - - client = snap7.client.Client() - client.connect('192.168.200.24', 0, 3) - - # this looks confusing but this means uploading from the PLC to YOU - # so downloading in the PC world :) - - all_data = client.upload(db_number) - - simple: - - db1 = snap7.util.DB( - db_number, # the db we use - all_data, # bytearray from the plc - layout, # layout specification DB variable data - # A DB specification is the specification of a - # DB object in the PLC you can find it using - # the dataview option on a DB object in PCS7 - - 17+2, # size of the specification 17 is start - # of last value - # which is a DWORD which is 2 bytes, - - 1, # number of row's / specifications - - id_field='ID', # field we can use to identify a row. - # default index is used - layout_offset=4, # sometimes specification does not start a 0 - # like in our example - db_offset=0 # At which point in 'all_data' should we start - # reading. if could be that the specification - # does not start at 0 - ) - - Now we can use db1 in python as a dict. if 'ID' contains - the 'test' we can identify the 'test' row in the all_data bytearray - - To test of you layout matches the data from the plc you can - just print db1[0] or db['test'] in the example - - db1['test']['testbool1'] = 0 - - If we do not specify a id_field this should work to read out the - same data. - - db1[0]['testbool1'] - - to read and write a single Row from the plc. takes like 5ms! - - db1['test'].write() - - db1['test'].read(client) - - -""" -import re -import time -import struct -import logging -from typing import Dict, Union, Callable, Optional, List -from datetime import date, datetime, timedelta -from collections import OrderedDict - -from .types import Areas -from .client import Client - -logger = logging.getLogger(__name__) - - -def utc2local(utc: Union[date, datetime]) -> Union[datetime, date]: - """Returns the local datetime - - Args: - utc: UTC type date or datetime. - - Returns: - Local datetime. - """ - epoch = time.mktime(utc.timetuple()) - offset = datetime.fromtimestamp(epoch) - datetime.utcfromtimestamp(epoch) - return utc + offset - - -def get_bool(bytearray_: bytearray, byte_index: int, bool_index: int) -> bool: - """Get the boolean value from location in bytearray - - Args: - bytearray_: buffer data. - byte_index: byte index to read from. - bool_index: bit index to read from. - - Returns: - True if the bit is 1, else 0. - - Examples: - >>> buffer = bytearray([0b00000001]) # Only one byte length - >>> get_bool(buffer, 0, 0) # The bit 0 starts at the right. - True - """ - index_value = 1 << bool_index - byte_value = bytearray_[byte_index] - current_value = byte_value & index_value - return current_value == index_value - - -def set_bool(bytearray_: bytearray, byte_index: int, bool_index: int, value: bool): - """Set boolean value on location in bytearray. - - Args: - bytearray_: buffer to write to. - byte_index: byte index to write to. - bool_index: bit index to write to. - value: value to write. - - Examples: - >>> buffer = bytearray([0b00000000]) - >>> set_bool(buffer, 0, 0, True) - >>> buffer - bytearray(b"\\x01") - """ - if value not in {0, 1, True, False}: - raise TypeError(f"Value value:{value} is not a boolean expression.") - - current_value = get_bool(bytearray_, byte_index, bool_index) - index_value = 1 << bool_index - - # check if bool already has correct value - if current_value == value: - return - - if value: - # make sure index_v is IN current byte - bytearray_[byte_index] += index_value - else: - # make sure index_v is NOT in current byte - bytearray_[byte_index] -= index_value - - -def set_byte(bytearray_: bytearray, byte_index: int, _int: int) -> bytearray: - """Set value in bytearray to byte - - Args: - bytearray_: buffer to write to. - byte_index: byte index to write. - _int: value to write. - - Returns: - buffer with the written value. - - Examples: - >>> buffer = bytearray([0b00000000]) - >>> set_byte(buffer, 0, 255) - bytearray(b"\\xFF") - """ - _int = int(_int) - _bytes = struct.pack('B', _int) - bytearray_[byte_index:byte_index + 1] = _bytes - return bytearray_ - - -def get_byte(bytearray_: bytearray, byte_index: int) -> bytes: - """Get byte value from bytearray. - - Notes: - WORD 8bit 1bytes Decimal number unsigned B#(0) to B#(255) => 0 to 255 - - Args: - bytearray_: buffer to be read from. - byte_index: byte index to be read. - - Returns: - value get from the byte index. - """ - data = bytearray_[byte_index:byte_index + 1] - data[0] = data[0] & 0xff - packed = struct.pack('B', *data) - value = struct.unpack('B', packed)[0] - return value - - -def set_word(bytearray_: bytearray, byte_index: int, _int: int): - """Set value in bytearray to word - - Notes: - Word datatype is 2 bytes long. - - Args: - bytearray_: buffer to be written. - byte_index: byte index to start write from. - _int: value to be write. - - Return: - buffer with the written value - """ - _int = int(_int) - _bytes = struct.unpack('2B', struct.pack('>H', _int)) - bytearray_[byte_index:byte_index + 2] = _bytes - return bytearray_ - - -def get_word(bytearray_: bytearray, byte_index: int) -> bytearray: - """Get word value from bytearray. - - Notes: - WORD 16bit 2bytes Decimal number unsigned B#(0,0) to B#(255,255) => 0 to 65535 - - Args: - bytearray_: buffer to get the word from. - byte_index: byte index from where start reading from. - - Returns: - Word value. - - Examples: - >>> data = bytearray([0, 100]) # two bytes for a word - >>> snap7.util.get_word(data, 0) - 100 - """ - data = bytearray_[byte_index:byte_index + 2] - data[1] = data[1] & 0xff - data[0] = data[0] & 0xff - packed = struct.pack('2B', *data) - value = struct.unpack('>H', packed)[0] - return value - - -def set_int(bytearray_: bytearray, byte_index: int, _int: int): - """Set value in bytearray to int - - Notes: - An datatype `int` in the PLC consists of two `bytes`. - - Args: - bytearray_: buffer to write on. - byte_index: byte index to start writing from. - _int: int value to write. - - Returns: - Buffer with the written value. - - Examples: - >>> data = bytearray(2) - >>> snap7.util.set_int(data, 0, 255) - bytearray(b'\\x00\\xff') - """ - # make sure were dealing with an int - _int = int(_int) - _bytes = struct.unpack('2B', struct.pack('>h', _int)) - bytearray_[byte_index:byte_index + 2] = _bytes - return bytearray_ - - -def get_int(bytearray_: bytearray, byte_index: int) -> int: - """Get int value from bytearray. - - Notes: - Datatype `int` in the PLC is represented in two bytes - - Args: - bytearray_: buffer to read from. - byte_index: byte index to start reading from. - - Returns: - Value read. - - Examples: - >>> data = bytearray([0, 255]) - >>> snap7.util.get_int(data, 0) - 255 - """ - data = bytearray_[byte_index:byte_index + 2] - data[1] = data[1] & 0xff - data[0] = data[0] & 0xff - packed = struct.pack('2B', *data) - value = struct.unpack('>h', packed)[0] - return value - - -def set_uint(bytearray_: bytearray, byte_index: int, _int: int): - """Set value in bytearray to unsigned int - - Notes: - An datatype `uint` in the PLC consists of two `bytes`. - - Args: - bytearray_: buffer to write on. - byte_index: byte index to start writing from. - _int: int value to write. - - Returns: - Buffer with the written value. - - Examples: - >>> data = bytearray(2) - >>> snap7.util.set_uint(data, 0, 65535) - bytearray(b'\\xff\\xff') - """ - # make sure were dealing with an int - _int = int(_int) - _bytes = struct.unpack('2B', struct.pack('>H', _int)) - bytearray_[byte_index:byte_index + 2] = _bytes - return bytearray_ - - -def get_uint(bytearray_: bytearray, byte_index: int) -> int: - """Get unsigned int value from bytearray. - - Notes: - Datatype `uint` in the PLC is represented in two bytes - Maximum posible value is 65535. - Lower posible value is 0. - - Args: - bytearray_: buffer to read from. - byte_index: byte index to start reading from. - - Returns: - Value read. - - Examples: - >>> data = bytearray([255, 255]) - >>> snap7.util.get_uint(data, 0) - 65535 - """ - data = bytearray_[byte_index:byte_index + 2] - data[1] = data[1] & 0xff - data[0] = data[0] & 0xff - packed = struct.pack('2B', *data) - value = struct.unpack('>H', packed)[0] - return value - - -def set_real(bytearray_: bytearray, byte_index: int, real) -> bytearray: - """Set Real value - - Notes: - Datatype `real` is represented in 4 bytes in the PLC. - The packed representation uses the `IEEE 754 binary32`. - - Args: - bytearray_: buffer to write to. - byte_index: byte index to start writing from. - real: value to be written. - - Returns: - Buffer with the value written. - - Examples: - >>> data = bytearray(4) - >>> snap7.util.set_real(data, 0, 123.321) - bytearray(b'B\\xf6\\xa4Z') - """ - real = float(real) - real = struct.pack('>f', real) - _bytes = struct.unpack('4B', real) - for i, b in enumerate(_bytes): - bytearray_[byte_index + i] = b - return bytearray_ - - -def get_real(bytearray_: bytearray, byte_index: int) -> float: - """Get real value. - - Notes: - Datatype `real` is represented in 4 bytes in the PLC. - The packed representation uses the `IEEE 754 binary32`. - - Args: - bytearray_: buffer to read from. - byte_index: byte index to reading from. - - Returns: - Real value. - - Examples: - >>> data = bytearray(b'B\\xf6\\xa4Z') - >>> snap7.util.get_real(data, 0) - 123.32099914550781 - """ - x = bytearray_[byte_index:byte_index + 4] - real = struct.unpack('>f', struct.pack('4B', *x))[0] - return real - - -def set_fstring(bytearray_: bytearray, byte_index: int, value: str, max_length: int): - """Set space-padded fixed-length string value - - Args: - bytearray_: buffer to write to. - byte_index: byte index to start writing from. - value: string to write. - max_length: maximum string length, i.e. the fixed size of the string. - - Raises: - :obj:`TypeError`: if the `value` is not a :obj:`str`. - :obj:`ValueError`: if the length of the `value` is larger than the `max_size` - or 'value' contains non-ascii characters. - - Examples: - >>> data = bytearray(20) - >>> snap7.util.set_fstring(data, 0, "hello world", 15) - >>> data - bytearray(b'hello world \x00\x00\x00\x00\x00') - """ - if not value.isascii(): - raise ValueError("Value contains non-ascii values.") - # FAIL HARD WHEN trying to write too much data into PLC - size = len(value) - if size > max_length: - raise ValueError(f'size {size} > max_length {max_length} {value}') - - i = 0 - - # fill array which chr integers - for i, c in enumerate(value): - bytearray_[byte_index + i] = ord(c) - - # fill the rest with empty space - for r in range(i + 1, max_length): - bytearray_[byte_index + r] = ord(' ') - - -def set_string(bytearray_: bytearray, byte_index: int, value: str, max_size: int = 254): - """Set string value - - Args: - bytearray_: buffer to write to. - byte_index: byte index to start writing from. - value: string to write. - max_size: maximum possible string size, max. 254 as default. - - Raises: - :obj:`TypeError`: if the `value` is not a :obj:`str`. - :obj:`ValueError`: if the length of the `value` is larger than the `max_size` - or 'max_size' is greater than 254 or 'value' contains non-ascii characters. - - Examples: - >>> data = bytearray(20) - >>> snap7.util.set_string(data, 0, "hello world", 254) - >>> data - bytearray(b'\\xff\\x0bhello world\\x00\\x00\\x00\\x00\\x00\\x00\\x00') - """ - if not isinstance(value, str): - raise TypeError(f"Value value:{value} is not from Type string") - - if max_size > 254: - raise ValueError(f'max_size: {max_size} > max. allowed 254 chars') - if not value.isascii(): - raise ValueError("Value contains non-ascii values, which is not compatible with PLC Type STRING." - "Check encoding of value or try set_wstring() (utf-16 encoding needed).") - size = len(value) - # FAIL HARD WHEN trying to write too much data into PLC - if size > max_size: - raise ValueError(f'size {size} > max_size {max_size} {value}') - - # set max string size - bytearray_[byte_index] = max_size - - # set len count on first position - bytearray_[byte_index + 1] = len(value) - - i = 0 - - # fill array which chr integers - for i, c in enumerate(value): - bytearray_[byte_index + 2 + i] = ord(c) - - # fill the rest with empty space - for r in range(i + 1, bytearray_[byte_index] - 2): - bytearray_[byte_index + 2 + r] = ord(' ') - - -def get_fstring(bytearray_: bytearray, byte_index: int, max_length: int, remove_padding: bool = True) -> str: - """Parse space-padded fixed-length string from bytearray - - Notes: - This function supports fixed-length ASCII strings, right-padded with spaces. - - Args: - bytearray_: buffer from where to get the string. - byte_index: byte index from where to start reading. - max_length: the maximum length of the string. - remove_padding: whether to remove the right-padding. - - Returns: - String value. - - Examples: - >>> data = [ord(letter) for letter in "hello world "] - >>> snap7.util.get_fstring(data, 0, 15) - 'hello world' - >>> snap7.util.get_fstring(data, 0, 15, remove_padding=false) - 'hello world ' - """ - data = map(chr, bytearray_[byte_index:byte_index + max_length]) - string = "".join(data) - - if remove_padding: - return string.rstrip(' ') - else: - return string - - -def get_string(bytearray_: bytearray, byte_index: int) -> str: - """Parse string from bytearray - - Notes: - The first byte of the buffer will contain the max size posible for a string. - The second byte contains the length of the string that contains. - - Args: - bytearray_: buffer from where to get the string. - byte_index: byte index from where to start reading. - - Returns: - String value. - - Examples: - >>> data = bytearray([254, len("hello world")] + [ord(letter) for letter in "hello world"]) - >>> snap7.util.get_string(data, 0) - 'hello world' - """ - - str_length = int(bytearray_[byte_index + 1]) - max_string_size = int(bytearray_[byte_index]) - - if str_length > max_string_size or max_string_size > 254: - logger.error("The string is too big for the size encountered in specification") - logger.error("WRONG SIZED STRING ENCOUNTERED") - raise TypeError("String contains {} chars, but max. {} chars are expected or is larger than 254." - "Bytearray doesn't seem to be a valid string.".format(str_length, max_string_size)) - data = map(chr, bytearray_[byte_index + 2:byte_index + 2 + str_length]) - return "".join(data) - - -def get_dword(bytearray_: bytearray, byte_index: int) -> int: - """ Gets the dword from the buffer. - - Notes: - Datatype `dword` consists in 8 bytes in the PLC. - The maximum value posible is `4294967295` - - Args: - bytearray_: buffer to read. - byte_index: byte index from where to start reading. - - Returns: - Value read. - - Examples: - >>> data = bytearray(8) - >>> data[:] = b"\\x12\\x34\\xAB\\xCD" - >>> snap7.util.get_dword(data, 0) - 4294967295 - """ - data = bytearray_[byte_index:byte_index + 4] - dword = struct.unpack('>I', struct.pack('4B', *data))[0] - return dword - - -def set_dword(bytearray_: bytearray, byte_index: int, dword: int): - """Set a DWORD to the buffer. - - Notes: - Datatype `dword` consists in 8 bytes in the PLC. - The maximum value posible is `4294967295` - - Args: - bytearray_: buffer to write to. - byte_index: byte index from where to writing reading. - dword: value to write. - - Examples: - >>> data = bytearray(4) - >>> snap7.util.set_dword(data,0, 4294967295) - >>> data - bytearray(b'\\xff\\xff\\xff\\xff') - """ - dword = int(dword) - _bytes = struct.unpack('4B', struct.pack('>I', dword)) - for i, b in enumerate(_bytes): - bytearray_[byte_index + i] = b - - -def get_dint(bytearray_: bytearray, byte_index: int) -> int: - """Get dint value from bytearray. - - Notes: - Datatype `dint` consists in 4 bytes in the PLC. - Maximum possible value is 2147483647. - Lower posible value is -2147483648. - - Args: - bytearray_: buffer to read. - byte_index: byte index from where to start reading. - - Returns: - Value read. - - Examples: - >>> import struct - >>> data = bytearray(4) - >>> data[:] = struct.pack(">i", 2147483647) - >>> snap7.util.get_dint(data, 0) - 2147483647 - """ - data = bytearray_[byte_index:byte_index + 4] - dint = struct.unpack('>i', struct.pack('4B', *data))[0] - return dint - - -def set_dint(bytearray_: bytearray, byte_index: int, dint: int): - """Set value in bytearray to dint - - Notes: - Datatype `dint` consists in 4 bytes in the PLC. - Maximum possible value is 2147483647. - Lower posible value is -2147483648. - - Args: - bytearray_: buffer to write. - byte_index: byte index from where to start writing. - dint: double integer value - - Examples: - >>> data = bytearray(4) - >>> snap7.util.set_dint(data, 0, 2147483647) - >>> data - bytearray(b'\\x7f\\xff\\xff\\xff') - """ - dint = int(dint) - _bytes = struct.unpack('4B', struct.pack('>i', dint)) - for i, b in enumerate(_bytes): - bytearray_[byte_index + i] = b - - -def get_udint(bytearray_: bytearray, byte_index: int) -> int: - """Get unsigned dint value from bytearray. - - Notes: - Datatype `udint` consists in 4 bytes in the PLC. - Maximum possible value is 4294967295. - Minimum posible value is 0. - - Args: - bytearray_: buffer to read. - byte_index: byte index from where to start reading. - - Returns: - Value read. - - Examples: - >>> import struct - >>> data = bytearray(4) - >>> data[:] = struct.pack(">I", 4294967295) - >>> snap7.util.get_udint(data, 0) - 4294967295 - """ - data = bytearray_[byte_index:byte_index + 4] - dint = struct.unpack('>I', struct.pack('4B', *data))[0] - return dint - - -def set_udint(bytearray_: bytearray, byte_index: int, udint: int): - """Set value in bytearray to unsigned dint - - Notes: - Datatype `dint` consists in 4 bytes in the PLC. - Maximum possible value is 4294967295. - Minimum posible value is 0. - - Args: - bytearray_: buffer to write. - byte_index: byte index from where to start writing. - udint: unsigned double integer value - - Examples: - >>> data = bytearray(4) - >>> snap7.util.set_udint(data, 0, 4294967295) - >>> data - bytearray(b'\\xff\\xff\\xff\\xff') - """ - udint = int(udint) - _bytes = struct.unpack('4B', struct.pack('>I', udint)) - for i, b in enumerate(_bytes): - bytearray_[byte_index + i] = b - - -def get_s5time(bytearray_: bytearray, byte_index: int) -> str: - micro_to_milli = 1000 - data_bytearray = bytearray_[byte_index:byte_index + 2] - s5time_data_int_like = list(data_bytearray.hex()) - if s5time_data_int_like[0] == '0': - # 10ms - time_base = 10 - elif s5time_data_int_like[0] == '1': - # 100ms - time_base = 100 - elif s5time_data_int_like[0] == '2': - # 1s - time_base = 1000 - elif s5time_data_int_like[0] == '3': - # 10s - time_base = 10000 - else: - raise ValueError('This value should not be greater than 3') - - s5time_bcd = \ - int(s5time_data_int_like[1]) * 100 + \ - int(s5time_data_int_like[2]) * 10 + \ - int(s5time_data_int_like[3]) - s5time_microseconds = time_base * s5time_bcd - s5time = timedelta(microseconds=s5time_microseconds * micro_to_milli) - # here we must return a string like variable, otherwise nothing will return - return "".join(str(s5time)) - - -def get_dt(bytearray_: bytearray, byte_index: int) -> str: - """Get DATE_AND_TIME Value from bytearray as ISO 8601 formatted Date String - Notes: - Datatype `DATE_AND_TIME` consists in 8 bytes in the PLC. - Args: - bytearray_: buffer to read. - byte_index: byte index from where to start writing. - Examples: - >>> data = bytearray(8) - >>> data[:] = [32, 7, 18, 23, 50, 2, 133, 65] #'2020-07-12T17:32:02.854000' - >>> get_dt(data,0) - '2020-07-12T17:32:02.854000' - """ - return get_date_time_object(bytearray_, byte_index).isoformat(timespec='microseconds') - - -def get_date_time_object(bytearray_: bytearray, byte_index: int) -> datetime: - """Get DATE_AND_TIME Value from bytearray as python datetime object - Notes: - Datatype `DATE_AND_TIME` consists in 8 bytes in the PLC. - Args: - bytearray_: buffer to read. - byte_index: byte index from where to start writing. - Examples: - >>> data = bytearray(8) - >>> data[:] = [32, 7, 18, 23, 50, 2, 133, 65] #date '2020-07-12 17:32:02.854' - >>> get_date_time_object(data,0) - datetime.datetime(2020, 7, 12, 17, 32, 2, 854000) - """ - - def bcd_to_byte(byte: int) -> int: - return (byte >> 4) * 10 + (byte & 0xF) - - year = bcd_to_byte(bytearray_[byte_index]) - # between 1990 and 2089, only last two digits are saved in DB 90 - 89 - year = 2000 + year if year < 90 else 1900 + year - month = bcd_to_byte(bytearray_[byte_index + 1]) - day = bcd_to_byte(bytearray_[byte_index + 2]) - hour = bcd_to_byte(bytearray_[byte_index + 3]) - min_ = bcd_to_byte(bytearray_[byte_index + 4]) - sec = bcd_to_byte(bytearray_[byte_index + 5]) - # plc save miliseconds in two bytes with the most signifanct byte used only - # in the last byte for microseconds the other for weekday - # * 1000 because pythoin datetime needs microseconds not milli - microsec = (bcd_to_byte(bytearray_[byte_index + 6]) * 10 - + bcd_to_byte(bytearray_[byte_index + 7] >> 4)) * 1000 - - return datetime(year, month, day, hour, min_, sec, microsec) - - -def get_time(bytearray_: bytearray, byte_index: int) -> str: - """Get time value from bytearray. - - Notes: - Datatype `time` consists in 4 bytes in the PLC. - Maximum possible value is T#24D_20H_31M_23S_647MS(2147483647). - Lower posible value is T#-24D_20H_31M_23S_648MS(-2147483648). - - Args: - bytearray_: buffer to read. - byte_index: byte index from where to start reading. - - Returns: - Value read. - - Examples: - >>> import struct - >>> data = bytearray(4) - >>> data[:] = struct.pack(">i", 2147483647) - >>> snap7.util.get_time(data, 0) - '24:20:31:23:647' - """ - data_bytearray = bytearray_[byte_index:byte_index + 4] - bits = 32 - sign = 1 - byte_str = data_bytearray.hex() - val = int(byte_str, 16) - if (val & (1 << (bits - 1))) != 0: - sign = -1 # if sign bit is set e.g., 8bit: 128-255 - val -= (1 << bits) # compute negative value - val *= sign - - milli_seconds = val % 1000 - seconds = val // 1000 - minutes = seconds // 60 - hours = minutes // 60 - days = hours // 24 - - sign_str = '' if sign >= 0 else '-' - time_str = f"{sign_str}{days!s}:{hours % 24!s}:{minutes % 60!s}:{seconds % 60!s}.{milli_seconds!s}" - - return time_str - - -def set_time(bytearray_: bytearray, byte_index: int, time_string: str) -> bytearray: - """Set value in bytearray to time - - Notes: - Datatype `time` consists in 4 bytes in the PLC. - Maximum possible value is T#24D_20H_31M_23S_647MS(2147483647). - Lower posible value is T#-24D_20H_31M_23S_648MS(-2147483648). - - Args: - bytearray_: buffer to write. - byte_index: byte index from where to start writing. - time_string: time value in string - - Examples: - >>> data = bytearray(4) - - >>> snap7.util.set_time(data, 0, '-22:3:57:28.192') - - >>> data - bytearray(b'\x8d\xda\xaf\x00') - """ - sign = 1 - if re.fullmatch(r"(-?(2[0-3]|1?\d):(2[0-3]|1?\d|\d):([1-5]?\d):[1-5]?\d.\d{1,3})|" - r"(-24:(20|1?\d):(3[0-1]|[0-2]?\d):(2[0-3]|1?\d).(64[0-8]|6[0-3]\d|[0-5]\d{1,2}))|" - r"(24:(20|1?\d):(3[0-1]|[0-2]?\d):(2[0-3]|1?\d).(64[0-7]|6[0-3]\d|[0-5]\d{1,2}))", time_string): - data_list = re.split('[: .]', time_string) - days: str = data_list[0] - hours: int = int(data_list[1]) - minutes: int = int(data_list[2]) - seconds: int = int(data_list[3]) - milli_seconds: int = int(data_list[4].ljust(3, '0')) - if re.match(r'^-\d{1,2}$', days): - sign = -1 - - time_int = ( - (int(days) * sign * 3600 * 24 + (hours % 24) * 3600 + (minutes % 60) * 60 + seconds % 60) * 1000 + milli_seconds - ) * sign - bytes_array = time_int.to_bytes(4, byteorder='big', signed=True) - bytearray_[byte_index:byte_index + 4] = bytes_array - return bytearray_ - else: - raise ValueError('time value out of range, please check the value interval') - - -def set_usint(bytearray_: bytearray, byte_index: int, _int: int) -> bytearray: - """Set unsigned small int - - Notes: - Datatype `usint` (Unsigned small int) consists on 1 byte in the PLC. - Maximum posible value is 255. - Lower posible value is 0. - - Args: - bytearray_: buffer to write. - byte_index: byte index from where to start writing. - _int: value to write. - - Returns: - Buffer with the written value. - - Examples: - >>> data = bytearray(1) - >>> snap7.util.set_usint(data, 0, 255) - bytearray(b'\\xff') - """ - _int = int(_int) - _bytes = struct.unpack('B', struct.pack('>B', _int)) - bytearray_[byte_index] = _bytes[0] - return bytearray_ - - -def get_usint(bytearray_: bytearray, byte_index: int) -> int: - """Get the unsigned small int from the bytearray - - Notes: - Datatype `usint` (Unsigned small int) consists on 1 byte in the PLC. - Maximum posible value is 255. - Lower posible value is 0. - - Args: - bytearray_: buffer to read from. - byte_index: byte index from where to start reading. - - Returns: - Value read. - - Examples: - >>> data = bytearray([255]) - >>> snap7.util.get_usint(data, 0) - 255 - """ - data = bytearray_[byte_index] & 0xff - packed = struct.pack('B', data) - value = struct.unpack('>B', packed)[0] - return value - - -def set_sint(bytearray_: bytearray, byte_index: int, _int) -> bytearray: - """Set small int to the buffer. - - Notes: - Datatype `sint` (Small int) consists in 1 byte in the PLC. - Maximum value posible is 127. - Lowest value posible is -128. - - Args: - bytearray_: buffer to write to. - byte_index: byte index from where to start writing. - _int: value to write. - - Returns: - Buffer with the written value. - - Examples: - >>> data = bytearray(1) - >>> snap7.util.set_sint(data, 0, 127) - bytearray(b'\\x7f') - """ - _int = int(_int) - _bytes = struct.unpack('B', struct.pack('>b', _int)) - bytearray_[byte_index] = _bytes[0] - return bytearray_ - - -def get_sint(bytearray_: bytearray, byte_index: int) -> int: - """Get the small int - - Notes: - Datatype `sint` (Small int) consists in 1 byte in the PLC. - Maximum value posible is 127. - Lowest value posible is -128. - - Args: - bytearray_: buffer to read from. - byte_index: byte index from where to start reading. - - Returns: - Value read. - - Examples: - >>> data = bytearray([127]) - >>> snap7.util.get_sint(data, 0) - 127 - """ - data = bytearray_[byte_index] - packed = struct.pack('B', data) - value = struct.unpack('>b', packed)[0] - return value - - -def get_lint(bytearray_: bytearray, byte_index: int): - """Get the long int - - THIS VALUE IS NEITHER TESTED NOR VERIFIED BY A REAL PLC AT THE MOMENT - - Notes: - Datatype `lint` (long int) consists in 8 bytes in the PLC. - Maximum value posible is +9223372036854775807 - Lowest value posible is -9223372036854775808 - - Args: - bytearray_: buffer to read from. - byte_index: byte index from where to start reading. - - Returns: - Value read. - - Examples: - read lint value (here as example 12345) from DB1.10 of a PLC - >>> data = client.db_read(db_number=1, start=10, size=8) - >>> snap7.util.get_lint(data, 0) - 12345 - """ - - # raw_lint = bytearray_[byte_index:byte_index + 8] - # lint = struct.unpack('>q', struct.pack('8B', *raw_lint))[0] - # return lint - return NotImplementedError - - -def get_lreal(bytearray_: bytearray, byte_index: int) -> float: - """Get the long real - - Notes: - Datatype `lreal` (long real) consists in 8 bytes in the PLC. - Negative Range: -1.7976931348623158e+308 to -2.2250738585072014e-308 - Positive Range: +2.2250738585072014e-308 to +1.7976931348623158e+308 - Zero: ±0 - - Args: - bytearray_: buffer to read from. - byte_index: byte index from where to start reading. - - Returns: - Value read. - - Examples: - read lreal value (here as example 12345.12345) from DB1.10 of a PLC - >>> data = client.db_read(db_number=1, start=10, size=8) - >>> snap7.util.get_lreal(data, 0) - 12345.12345 - """ - return struct.unpack_from(">d", bytearray_, offset=byte_index)[0] - - -def set_lreal(bytearray_: bytearray, byte_index: int, lreal) -> bytearray: - """Set the long real - - Notes: - Datatype `lreal` (long real) consists in 8 bytes in the PLC. - Negative Range: -1.7976931348623158e+308 to -2.2250738585072014e-308 - Positive Range: +2.2250738585072014e-308 to +1.7976931348623158e+308 - Zero: ±0 - - Args: - bytearray_: buffer to read from. - byte_index: byte index from where to start reading. - lreal: float value to set - - Returns: - Value to write. - - Examples: - write lreal value (here as example 12345.12345) to DB1.10 of a PLC - >>> data = snap7.util.set_lreal(data, 12345.12345) - >>> client.db_write(db_number=1, start=10, data) - - """ - lreal = float(lreal) - struct.pack_into(">d", bytearray_, byte_index, lreal) - return bytearray_ - - -def get_lword(bytearray_: bytearray, byte_index: int) -> bytearray: - """Get the long word - - THIS VALUE IS NEITHER TESTED NOR VERIFIED BY A REAL PLC AT THE MOMENT - - Notes: - Datatype `lword` (long word) consists in 8 bytes in the PLC. - Maximum value posible is bytearray(b"\\xFF\\xFF\\xFF\\xFF\\xFF\\xFF\\xFF\\xFF") - Lowest value posible is bytearray(b"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00") - - Args: - bytearray_: buffer to read from. - byte_index: byte index from where to start reading. - - Returns: - Value read. - - Examples: - read lword value (here as example 0xAB\0xCD) from DB1.10 of a PLC - >>> data = client.db_read(db_number=1, start=10, size=8) - >>> snap7.util.get_lword(data, 0) - bytearray(b"\\x00\\x00\\x00\\x00\\x00\\x00\\xAB\\xCD") - """ - # data = bytearray_[byte_index:byte_index + 4] - # dword = struct.unpack('>Q', struct.pack('8B', *data))[0] - # return bytearray(dword) - raise NotImplementedError - - -def set_lword(bytearray_: bytearray, byte_index: int, lword: bytearray) -> bytearray: - """Set the long word - - THIS VALUE IS NEITHER TESTED NOR VERIFIED BY A REAL PLC AT THE MOMENT - - Notes: - Datatype `lword` (long word) consists in 8 bytes in the PLC. - Maximum value posible is bytearray(b"\\xFF\\xFF\\xFF\\xFF\\xFF\\xFF\\xFF\\xFF") - Lowest value posible is bytearray(b"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00") - - Args: - bytearray_: buffer to read from. - byte_index: byte index from where to start reading. - lword: Value to write - - Returns: - Bytearray conform value. - - Examples: - read lword value (here as example 0xAB\0xCD) from DB1.10 of a PLC - >>> data = snap7.util.set_lword(data, 0, bytearray(b"\\x00\\x00\\x00\\x00\\x00\\x00\\xAB\\xCD")) - bytearray(b"\\x00\\x00\\x00\\x00\\x00\\x00\\xAB\\xCD") - >>> client.db_write(db_number=1, start=10, data) - """ - # data = bytearray_[byte_index:byte_index + 4] - # dword = struct.unpack('8B', struct.pack('>Q', *data))[0] - # return bytearray(dword) - raise NotImplementedError - - -def get_ulint(bytearray_: bytearray, byte_index: int) -> int: - """Get ulint value from bytearray. - - Notes: - Datatype `int` in the PLC is represented in 8 bytes - - Args: - bytearray_: buffer to read from. - byte_index: byte index to start reading from. - - Returns: - Value read. - - Examples: - Read 8 Bytes raw from DB1.10, where an ulint value is stored. Return Python compatible value. - >>> data = client.db_read(db_number=1, start=10, size=8) - >>> snap7.util.get_ulint(data, 0) - 12345 - """ - raw_ulint = bytearray_[byte_index:byte_index + 8] - lint = struct.unpack('>Q', struct.pack('8B', *raw_ulint))[0] - return lint - - -def get_tod(bytearray_: bytearray, byte_index: int) -> timedelta: - len_bytearray_ = len(bytearray_) - byte_range = byte_index + 4 - if len_bytearray_ < byte_range: - raise ValueError("Date can't be extracted from bytearray. bytearray_[Index:Index+16] would cause overflow.") - time_val = timedelta(milliseconds=int.from_bytes(bytearray_[byte_index:byte_range], byteorder='big')) - if time_val.days >= 1: - raise ValueError("Time_Of_Date can't be extracted from bytearray. Bytearray contains unexpected values.") - return time_val - - -def get_date(bytearray_: bytearray, byte_index: int = 0) -> date: - len_bytearray_ = len(bytearray_) - byte_range = byte_index + 2 - if len_bytearray_ < byte_range: - raise ValueError("Date can't be extracted from bytearray. bytearray_[Index:Index+16] would cause overflow.") - date_val = date(1990, 1, 1) + timedelta(days=int.from_bytes(bytearray_[byte_index:byte_range], byteorder='big')) - if date_val > date(2168, 12, 31): - raise ValueError("date_val is higher than specification allows.") - return date_val - - -def get_ltime(bytearray_: bytearray, byte_index: int) -> str: - raise NotImplementedError - - -def get_ltod(bytearray_: bytearray, byte_index: int) -> str: - raise NotImplementedError - - -def get_ldt(bytearray_: bytearray, byte_index: int) -> str: - raise NotImplementedError - - -def get_dtl(bytearray_: bytearray, byte_index: int) -> datetime: - time_to_datetime = datetime( - year=int.from_bytes(bytearray_[byte_index:byte_index + 2], byteorder='big'), - month=int(bytearray_[byte_index + 2]), - day=int(bytearray_[byte_index + 3]), - hour=int(bytearray_[byte_index + 5]), - minute=int(bytearray_[byte_index + 6]), - second=int(bytearray_[byte_index + 7]), - microsecond=int(bytearray_[byte_index + 8])) # --- ? noch nicht genau genug - if time_to_datetime > datetime(2554, 12, 31, 23, 59, 59): - raise ValueError("date_val is higher than specification allows.") - return time_to_datetime - - -def get_char(bytearray_: bytearray, byte_index: int) -> str: - """Get char value from bytearray. - - Notes: - Datatype `char` in the PLC is represented in 1 byte. It has to be in ASCII-format. - - Args: - bytearray_: buffer to read from. - byte_index: byte index to start reading from. - - Returns: - Value read. - - Examples: - Read 1 Byte raw from DB1.10, where a char value is stored. Return Python compatible value. - >>> data = client.db_read(db_number=1, start=10, size=1) - >>> snap7.util.get_char(data, 0) - 'C' - """ - char = chr(bytearray_[byte_index]) - return char - - -def set_char(bytearray_: bytearray, byte_index: int, chr_: str) -> Union[ValueError, bytearray]: - """Set char value in a bytearray. - - Notes: - Datatype `char` in the PLC is represented in 1 byte. It has to be in ASCII-format - - Args: - bytearray_: buffer to read from. - byte_index: byte index to start reading from. - chr_: Char to be set - - Returns: - Value read. - - Examples: - Read 1 Byte raw from DB1.10, where a char value is stored. Return Python compatible value. - >>> data = snap7.util.set_char(data, 0, 'C') - >>> client.db_write(db_number=1, start=10, data) - 'bytearray('0x43') - """ - if chr_.isascii(): - bytearray_[byte_index] = ord(chr_) - return bytearray_ - raise ValueError(f"chr_ : {chr_} contains a None-Ascii value, but ASCII-only is allowed.") - - -def get_wchar(bytearray_: bytearray, byte_index: int) -> Union[ValueError, str]: - """Get wchar value from bytearray. - - Notes: - Datatype `wchar` in the PLC is represented in 2 bytes. It has to be in utf-16-be format. - - - Args: - bytearray_: buffer to read from. - byte_index: byte index to start reading from. - - Returns: - Value read. - - Examples: - Read 2 Bytes raw from DB1.10, where a wchar value is stored. Return Python compatible value. - >>> data = client.db_read(db_number=1, start=10, size=2) - >>> snap7.util.get_wchar(data, 0) - 'C' - """ - if bytearray_[byte_index] == 0: - return chr(bytearray_[1]) - return bytearray_[byte_index:byte_index + 2].decode('utf-16-be') - - -def get_wstring(bytearray_: bytearray, byte_index: int) -> str: - """Parse wstring from bytearray - - Notes: - Byte 0 and 1 contains the max size posible for a string (2 Byte value). - byte 2 and 3 contains the length of the string that contains (2 Byte value). - The other bytes contain WCHARs (2Byte) in utf-16-be style. - - Args: - bytearray_: buffer from where to get the string. - byte_index: byte index from where to start reading. - - Returns: - String value. - - Examples: - Read from DB1.10 22, where the WSTRING is stored, the raw 22 Bytes and convert them to a python string - >>> data = client.db_read(db_number=1, start=10, size=22) - >>> snap7.util.get_wstring(data, 0) - 'hello world' - """ - # Byte 0 + 1 --> total length of wstring, should be bytearray_ - 4 - # Byte 2, 3 --> used length of wstring - wstring_start = byte_index + 4 - - max_wstring_size = bytearray_[byte_index:byte_index + 2] - packed = struct.pack('2B', *max_wstring_size) - max_wstring_symbols = struct.unpack('>H', packed)[0] * 2 - - wstr_length_raw = bytearray_[byte_index + 2:byte_index + 4] - wstr_symbols_amount = struct.unpack('>H', struct.pack('2B', *wstr_length_raw))[0] * 2 - - if wstr_symbols_amount > max_wstring_symbols or max_wstring_symbols > 16382: - logger.error("The wstring is too big for the size encountered in specification") - logger.error("WRONG SIZED STRING ENCOUNTERED") - raise TypeError("WString contains {} chars, but max. {} chars are expected or is larger than 16382." - "Bytearray doesn't seem to be a valid string.".format(wstr_symbols_amount, max_wstring_symbols)) - - return bytearray_[wstring_start:wstring_start + wstr_symbols_amount].decode('utf-16-be') - - -def get_array(bytearray_: bytearray, byte_index: int) -> List: - raise NotImplementedError -# --------------------------- - - -def parse_specification(db_specification: str) -> OrderedDict: - """Create a db specification derived from a - dataview of a db in which the byte layout - is specified - - Args: - db_specification: string formatted table with the indexes, aliases and types. - - Returns: - Parsed DB specification. - """ - parsed_db_specification = OrderedDict() - - for line in db_specification.split('\n'): - if line and not line.lstrip().startswith('#'): - index, var_name, _type = line.split('#')[0].split() - parsed_db_specification[var_name] = (index, _type) - - return parsed_db_specification - - -class DB: - """ - Manage a DB bytearray block given a specification - of the Layout. - - It is possible to have many repetitive instances of - a specification this is called a "row". - - Probably most usecases there is just one row - - Note: - This class has some of the semantics of a dict. In particular, the membership operators - (``in``, ``not it``), the access operator (``[]``), as well as the :func:`~DB.keys()` and - :func:`~DB.items()` methods work as usual. Iteration, on the other hand, happens on items - instead of keys (much like :func:`~DB.items()` method). - - Attributes: - bytearray_: buffer data from the PLC. - specification: layout of the DB Rows. - row_size: bytes size of a db row. - layout_offset: at which byte in the row specificaion we - start reading the data. - db_offset: at which byte in the db starts reading. - - Examples: - >>> db1[0]['testbool1'] = test - >>> db1.write(client) # puts data in plc - """ - bytearray_: Optional[bytearray] = None # data from plc - specification: Optional[str] = None # layout of db rows - id_field: Optional[str] = None # ID field of the rows - row_size: int = 0 # bytes size of a db row - layout_offset: int = 0 # at which byte in row specification should - db_offset: int = 0 # at which byte in db should we start reading? - - # first fields could be be status data. - # and only the last part could be control data - # now you can be sure you will never overwrite - # critical parts of db - - def __init__(self, db_number: int, bytearray_: bytearray, - specification: str, row_size: int, size: int, id_field: Optional[str] = None, - db_offset: int = 0, layout_offset: int = 0, row_offset: int = 0, - area: Areas = Areas.DB): - """ Creates a new instance of the `Row` class. - - Args: - db_number: number of the DB to read from. This value should be 0 if area!=Areas.DB. - bytearray_: initial buffer read from the PLC. - specification: layout of the PLC memory. - row_size: bytes size of a db row. - size: lenght of the memory area. - id_field: name to reference the row. Optional. - db_offset: at which byte in the db starts reading. - layout_offset: at which byte in the row specificaion we - start reading the data. - row_offset: offset between rows. - area: which memory area this row is representing. - """ - self.db_number = db_number - self.size = size - self.row_size = row_size - self.id_field = id_field - self.area = area - - self.db_offset = db_offset - self.layout_offset = layout_offset - self.row_offset = row_offset - - self._bytearray = bytearray_ - self.specification = specification - # loop over bytearray. make rowObjects - # store index of id_field to row objects - self.index: OrderedDict = OrderedDict() - self.make_rows() - - def make_rows(self): - """ Make each row for the DB.""" - id_field = self.id_field - row_size = self.row_size - specification = self.specification - layout_offset = self.layout_offset - row_offset = self.row_offset - - for i in range(self.size): - # calculate where row in bytearray starts - db_offset = i * (row_size + row_offset) + self.db_offset - # create a row object - row = DB_Row(self, - specification, - row_size=row_size, - db_offset=db_offset, - layout_offset=layout_offset, - row_offset=self.row_offset, - area=self.area) - - # store row object - key = row[id_field] if id_field else i - if key and key in self.index: - msg = f'{key} not unique!' - logger.error(msg) - self.index[key] = row - - def __getitem__(self, key: str, default: Optional[None] = None) -> Union[None, int, float, str, bool, datetime]: - """Access a row of the table through its index. - - Rows (values) are of type :class:`DB_Row`. - - Notes: - This method has the same semantics as :class:`dict` access. - """ - return self.index.get(key, default) - - def __iter__(self): - """Iterate over the items contained in the table, in the physical order they are contained - in memory. - - Notes: - This method does not have the same semantics as :class:`dict` iteration. Instead, it - has the same semantics as the :func:`~DB.items` method, yielding ``(index, row)`` - tuples. - """ - yield from self.index.items() - - def __len__(self): - """Return the number of rows contained in the DB. - - Notes: - If more than one row has the same index value, it is only counted once. - """ - return len(self.index) - - def __contains__(self, key): - """Return whether the given key is the index of a row in the DB.""" - return key in self.index - - def keys(self): - """Return a *view object* of the keys that are used as indices for the rows in the - DB. - """ - yield from self.index.keys() - - def items(self): - """Return a *view object* of the items (``(index, row)`` pairs) that are used as indices - for the rows in the DB. - """ - yield from self.index.items() - - def export(self): - """Export the object to an :class:`OrderedDict`, where each item in the dictionary - has an index as the key, and the value of the DB row associated with that index - as a value, represented itself as a :class:`dict` (as returned by :func:`DB_Row.export`). - - The outer dictionary contains the rows in the physical order they are contained in - memory. - - Notes: - This function effectively returns a snapshot of the DB. - """ - ret = OrderedDict() - for (k, v) in self.items(): - ret[k] = v.export() - return ret - - def set_data(self, bytearray_: bytearray): - """Set the new buffer data from the PLC to the current instance. - - Args: - bytearray_: buffer to save. - - Raises: - :obj:`TypeError`: if `bytearray_` is not an instance of :obj:`bytearray` - """ - if not isinstance(bytearray_, bytearray): - raise TypeError(f"Value bytearray_: {bytearray_} is not from type bytearray") - self._bytearray = bytearray_ - - def read(self, client: Client): - """Reads all the rows from the PLC to the :obj:`bytearray` of this instance. - - Args: - client: :obj:`Client` snap7 instance. - - Raises: - :obj:`ValueError`: if the `row_size` is less than 0. - """ - if self.row_size < 0: - raise ValueError("row_size must be greater equal zero.") - - total_size = self.size * (self.row_size + self.row_offset) - if self.area == Areas.DB: # note: is it worth using the upload method? - bytearray_ = client.db_read(self.db_number, self.db_offset, total_size) - else: - bytearray_ = client.read_area(self.area, 0, self.db_offset, total_size) - - # replace data in bytearray - for i, b in enumerate(bytearray_): - self._bytearray[i + self.db_offset] = b - - # todo: optimize by only rebuilding the index instead of all the DB_Row objects - self.index.clear() - self.make_rows() - - def write(self, client): - """Writes all the rows from the :obj:`bytearray` of this instance to the PLC - - Notes: - When the row_offset property has been set to something other than None while - constructing this object, this operation is not guaranteed to be atomic. - - Args: - client: :obj:`Client` snap7 instance. - - Raises: - :obj:`ValueError`: if the `row_size` is less than 0. - """ - if self.row_size < 0: - raise ValueError("row_size must be greater equal zero.") - - # special case: we have a row offset, so we must write each row individually - # this is because we don't want to change the data before the offset - if self.row_offset: - for _, v in self.index.items(): - v.write(client) - return - - total_size = self.size * (self.row_size + self.row_offset) - data = self._bytearray[self.db_offset:self.db_offset + total_size] - - if self.area == Areas.DB: - client.db_write(self.db_number, self.db_offset, data) - else: - client.write_area(self.area, 0, self.db_offset, data) - - -class DB_Row: - """ - Provide ROW API for DB bytearray - - Attributes: - bytearray_: reference to the data of the parent DB. - _specification: row specification layout. - """ - bytearray_: bytearray # data of reference to parent DB - _specification: OrderedDict = OrderedDict() # row specification - - def __init__( - self, - bytearray_: bytearray, - _specification: str, - row_size: Optional[int] = 0, - db_offset: int = 0, - layout_offset: int = 0, - row_offset: Optional[int] = 0, - area: Optional[Areas] = Areas.DB - ): - """Creates a new instance of the `DB_Row` class. - - Args: - bytearray_: reference to the data of the parent DB. - _specification: row specification layout. - row_size: Amount of bytes of the row. - db_offset: at which byte in the db starts reading. - layout_offset: at which byte in the row specificaion we - start reading the data. - row_offset: offset between rows. - area: which memory area this row is representing. - - Raises: - :obj:`TypeError`: if `bytearray_` is not an instance of :obj:`bytearray` or :obj:`DB`. - """ - - self.db_offset = db_offset # start point of row data in db - self.layout_offset = layout_offset # start point of row data in layout - self.row_size = row_size # lenght of the read - self.row_offset = row_offset # start of writable part of row - self.area = area - - if not isinstance(bytearray_, (bytearray, DB)): - raise TypeError(f"Value bytearray_ {bytearray_} is not from type (bytearray, DB)") - self._bytearray = bytearray_ - self._specification = parse_specification(_specification) - - def get_bytearray(self) -> bytearray: - """Gets bytearray from self or DB parent - - Returns: - Buffer data corresponding to the row. - """ - if isinstance(self._bytearray, DB): - return self._bytearray._bytearray - return self._bytearray - - def export(self) -> Dict[str, Union[str, int, float, bool, datetime]]: - """ Export dictionary with values - - Returns: - dictionary containing the values of each value of the row. - """ - return {key: self[key] for key in self._specification} - - def __getitem__(self, key): - """ - Get a specific db field - """ - index, _type = self._specification[key] - return self.get_value(index, _type) - - def __setitem__(self, key, value): - index, _type = self._specification[key] - self.set_value(index, _type, value) - - def __repr__(self): - - string = "" - for var_name, (index, _type) in self._specification.items(): - string = f'{string}\n{var_name:<20} {self.get_value(index, _type):<10}' - return string - - def unchanged(self, bytearray_: bytearray) -> bool: - """ Checks if the bytearray is the same - - Args: - bytearray_: buffer of data to check. - - Returns: - True if the current `bytearray_` is equal to the new one. Otherwise is False. - """ - return self.get_bytearray() == bytearray_ - - def get_offset(self, byte_index: Union[str, int]) -> int: - """ Calculate correct beginning position for a row - the db_offset = row_size * index - - Args: - byte_index: byte index from where to start reading from. - - Returns: - Amount of bytes to ignore. - """ - # add float typ to avoid error because of - # the variable address with decimal point(like 0.0 or 4.0) - return int(float(byte_index)) - self.layout_offset + self.db_offset - - def get_value(self, byte_index: Union[str, int], type_: str) -> Union[ValueError, int, float, str, datetime]: - """ Gets the value for a specific type. - - Args: - byte_index: byte index from where start reading. - type_: type of data to read. - - Raises: - :obj:`ValueError`: if reading a `string` when checking the lenght of the string. - :obj:`ValueError`: if the `type_` is not handled. - - Returns: - Value read according to the `type_` - """ - bytearray_ = self.get_bytearray() - - # set parsing non case-sensitive - type_ = type_.upper() - - if type_ == 'BOOL': - byte_index, bool_index = str(byte_index).split('.') - return get_bool(bytearray_, self.get_offset(byte_index), - int(bool_index)) - - # remove 4 from byte index since - # first 4 bytes are used by db - byte_index = self.get_offset(byte_index) - - if type_.startswith('FSTRING'): - max_size = re.search(r'\d+', type_) - if max_size is None: - raise ValueError("Max size could not be determinate. re.search() returned None") - return get_fstring(bytearray_, byte_index, int(max_size[0])) - elif type_.startswith('STRING'): - max_size = re.search(r'\d+', type_) - if max_size is None: - raise ValueError("Max size could not be determinate. re.search() returned None") - return get_string(bytearray_, byte_index) - elif type_.startswith('WSTRING'): - max_size = re.search(r'\d+', type_) - if max_size is None: - raise ValueError("Max size could not be determinate. re.search() returned None") - return get_wstring(bytearray_, byte_index) - else: - type_to_func: Dict[str, Callable] = { - 'REAL': get_real, - 'DWORD': get_dword, - 'UDINT': get_udint, - 'DINT': get_dint, - 'UINT': get_uint, - 'INT': get_int, - 'WORD': get_word, - 'BYTE': get_byte, - 'S5TIME': get_s5time, - 'DATE_AND_TIME': get_dt, - 'USINT': get_usint, - 'SINT': get_sint, - 'TIME': get_time, - 'DATE': get_date, - 'TIME_OF_DAY': get_tod, - 'LREAL': get_lreal, - 'TOD': get_tod, - 'CHAR': get_char, - 'WCHAR': get_wchar, - 'DTL': get_dtl - } - if type_ in type_to_func: - return type_to_func[type_](bytearray_, byte_index) - raise ValueError - - def set_value(self, byte_index: Union[str, int], type_: str, value: Union[bool, str, float]) -> Union[bytearray, None]: - """Sets the value for a specific type in the specified byte index. - - Args: - byte_index: byte index to start writing to. - type_: type of value to write. - value: value to write. - - Raises: - :obj:`ValueError`: if reading a `string` when checking the length of the string. - :obj:`ValueError`: if the `type_` is not handled. - - Returns: - Buffer data with the value written. Optional. - """ - - bytearray_ = self.get_bytearray() - - if type_ == 'BOOL' and isinstance(value, bool): - byte_index, bool_index = str(byte_index).split(".") - return set_bool(bytearray_, self.get_offset(byte_index), - int(bool_index), value) - - byte_index = self.get_offset(byte_index) - - if type_.startswith('FSTRING') and isinstance(value, str): - max_size = re.search(r'\d+', type_) - if max_size is None: - raise ValueError("Max size could not be determinate. re.search() returned None") - max_size_grouped = max_size.group(0) - max_size_int = int(max_size_grouped) - return set_fstring(bytearray_, byte_index, value, max_size_int) - - if type_.startswith('STRING') and isinstance(value, str): - max_size = re.search(r'\d+', type_) - if max_size is None: - raise ValueError("Max size could not be determinate. re.search() returned None") - max_size_grouped = max_size.group(0) - max_size_int = int(max_size_grouped) - return set_string(bytearray_, byte_index, value, max_size_int) - - if type_ == 'REAL': - return set_real(bytearray_, byte_index, value) - - if type_ == 'LREAL': - return set_lreal(bytearray_, byte_index, value) - - if isinstance(value, int): - type_to_func = { - 'DWORD': set_dword, - 'UDINT': set_udint, - 'DINT': set_dint, - 'UINT': set_uint, - 'INT': set_int, - 'WORD': set_word, - 'BYTE': set_byte, - 'USINT': set_usint, - 'SINT': set_sint, - } - if type_ in type_to_func: - return type_to_func[type_](bytearray_, byte_index, value) - - if type_ == 'TIME' and isinstance(value, str): - return set_time(bytearray_, byte_index, value) - - raise ValueError - - def write(self, client: Client) -> None: - """Write current data to db in plc - - Args: - client: :obj:`Client` snap7 instance. - - Raises: - :obj:`TypeError`: if the `_bytearray` is not an instance of :obj:`DB` class. - :obj:`ValueError`: if the `row_size` is less than 0. - """ - if not isinstance(self._bytearray, DB): - raise TypeError(f"Value self._bytearray: {self._bytearray} is not from type DB.") - if self.row_size < 0: - raise ValueError("row_size must be greater equal zero.") - - db_nr = self._bytearray.db_number - offset = self.db_offset - data = self.get_bytearray()[offset:offset + self.row_size] - db_offset = self.db_offset - - # indicate start of write only area of row! - if self.row_offset: - data = data[self.row_offset:] - db_offset += self.row_offset - - if self.area == Areas.DB: - client.db_write(db_nr, db_offset, data) - else: - client.write_area(self.area, 0, db_offset, data) - - def read(self, client: Client) -> None: - """Read current data of db row from plc. - - Args: - client: :obj:`Client` snap7 instance. - - Raises: - :obj:`TypeError`: if the `_bytearray` is not an instance of :obj:`DB` class. - :obj:`ValueError`: if the `row_size` is less than 0. - """ - if not isinstance(self._bytearray, DB): - raise TypeError(f"Value self._bytearray:{self._bytearray} is not from type DB.") - if self.row_size < 0: - raise ValueError("row_size must be greater equal zero.") - db_nr = self._bytearray.db_number - if self.area == Areas.DB: - bytearray_ = client.db_read(db_nr, self.db_offset, self.row_size) - else: - bytearray_ = client.read_area(self.area, 0, self.db_offset, self.row_size) - - data = self.get_bytearray() - # replace data in bytearray - for i, b in enumerate(bytearray_): - data[i + self.db_offset] = b diff --git a/snap7/util/__init__.py b/snap7/util/__init__.py new file mode 100644 index 00000000..f6776aed --- /dev/null +++ b/snap7/util/__init__.py @@ -0,0 +1,200 @@ +""" +This module contains utility functions for working with PLC DB objects. +There are functions to work with the raw bytearray data snap7 functions return +In order to work with this data you need to make python able to work with the +PLC bytearray data. + +For example code see test_util.py and example.py in the example folder. + + +example:: + + spec/DB layout + + # Byte index Variable name Datatype + layout=\"\"\" + 4 ID INT + 6 NAME STRING[6] + + 12.0 testbool1 BOOL + 12.1 testbool2 BOOL + 12.2 testbool3 BOOL + 12.3 testbool4 BOOL + 12.4 testbool5 BOOL + 12.5 testbool6 BOOL + 12.6 testbool7 BOOL + 12.7 testbool8 BOOL + 13 testReal REAL + 17 testDword DWORD + \"\"\" + + client = snap7.client.Client() + client.connect('192.168.200.24', 0, 3) + + # this looks confusing but this means uploading from the PLC to YOU + # so downloading in the PC world :) + + all_data = client.upload(db_number) + + simple: + + db1 = snap7.util.DB( + db_number, # the db we use + all_data, # bytearray from the plc + layout, # layout specification DB variable data + # A DB specification is the specification of a + # DB object in the PLC you can find it using + # the dataview option on a DB object in PCS7 + + 17+2, # size of the specification 17 is start + # of last value + # which is a DWORD which is 2 bytes, + + 1, # number of row's / specifications + + id_field='ID', # field we can use to identify a row. + # default index is used + layout_offset=4, # sometimes specification does not start a 0 + # like in our example + db_offset=0 # At which point in 'all_data' should we start + # reading. if could be that the specification + # does not start at 0 + ) + + Now we can use db1 in python as a dict. if 'ID' contains + the 'test' we can identify the 'test' row in the all_data bytearray + + To test of you layout matches the data from the plc you can + just print db1[0] or db['test'] in the example + + db1['test']['testbool1'] = 0 + + If we do not specify a id_field this should work to read out the + same data. + + db1[0]['testbool1'] + + to read and write a single Row from the plc. takes like 5ms! + + db1['test'].write() + + db1['test'].read(client) + + +""" + +import re +import time +from typing import Union +from datetime import date, datetime +from collections import OrderedDict + +from .setters import ( + set_bool, # noqa: F401 + set_fstring, # noqa: F401 + set_string, # noqa: F401 + set_real, # noqa: F401 + set_dword, # noqa: F401 + set_udint, # noqa: F401 + set_dint, # noqa: F401 + set_uint, # noqa: F401 + set_int, # noqa: F401 + set_word, # noqa: F401 + set_byte, # noqa: F401 + set_usint, # noqa: F401 + set_sint, # noqa: F401 + set_time, # noqa: F401 +) + +from .getters import ( + get_bool, # noqa: F401 + get_fstring, # noqa: F401 + get_string, # noqa: F401 + get_wstring, # noqa: F401 + get_real, # noqa: F401 + get_dword, # noqa: F401 + get_udint, # noqa: F401 + get_dint, # noqa: F401 + get_uint, # noqa: F401 + get_int, # noqa: F401 + get_word, # noqa: F401 + get_byte, # noqa: F401 + get_s5time, # noqa: F401 + get_dt, # noqa: F401 + get_usint, # noqa: F401 + get_sint, # noqa: F401 + get_time, # noqa: F401 + get_date, # noqa: F401 + get_tod, # noqa: F401 + get_lreal, # noqa: F401 + get_char, # noqa: F401 + get_wchar, # noqa: F401 + get_dtl, # noqa: F401 +) + + +def utc2local(utc: Union[date, datetime]) -> Union[datetime, date]: + """Returns the local datetime + + Args: + utc: UTC type date or datetime. + + Returns: + Local datetime. + """ + epoch = time.mktime(utc.timetuple()) + offset = datetime.fromtimestamp(epoch) - datetime.utcfromtimestamp(epoch) + return utc + offset + + +def parse_specification(db_specification: str) -> OrderedDict: + """Create a db specification derived from a + dataview of a db in which the byte layout + is specified + + Args: + db_specification: string formatted table with the indexes, aliases and types. + + Returns: + Parsed DB specification. + """ + parsed_db_specification = OrderedDict() + + for line in db_specification.split("\n"): + if line and not line.lstrip().startswith("#"): + index, var_name, _type = line.split("#")[0].split() + parsed_db_specification[var_name] = (index, _type) + + return parsed_db_specification + + +def print_row(data): + """print a single db row in chr and str""" + index_line = "" + pri_line1 = "" + chr_line2 = "" + asci = re.compile("[a-zA-Z0-9 ]") + + for i, xi in enumerate(data): + # index + if not i % 5: + diff = len(pri_line1) - len(index_line) + i = str(i) + index_line += diff * " " + index_line += i + # i = i + (ws - len(i)) * ' ' + ',' + + # byte array line + str_v = str(xi) + pri_line1 += str(xi) + "," + # char line + c = chr(xi) + c = c if asci.match(c) else " " + # align white space + w = len(str_v) + c = c + (w - 1) * " " + "," + chr_line2 += c + + print(index_line) + print(pri_line1) + print(chr_line2) diff --git a/snap7/util/db.py b/snap7/util/db.py new file mode 100644 index 00000000..f782e882 --- /dev/null +++ b/snap7/util/db.py @@ -0,0 +1,598 @@ +import re +from collections import OrderedDict +from datetime import datetime +from typing import Optional, Union, Dict, Callable +from logging import getLogger + +from snap7.client import Client +from snap7.types import Areas +from snap7.util import ( + parse_specification, + get_bool, + get_fstring, + get_string, + get_wstring, + get_real, + get_dword, + get_udint, + get_dint, + get_uint, + get_int, + get_word, + get_byte, + get_s5time, + get_dt, + get_usint, + get_sint, + get_time, + get_date, + get_tod, + get_lreal, + get_char, + get_wchar, + get_dtl, + set_bool, + set_fstring, + set_string, + set_real, + set_dword, + set_udint, + set_dint, + set_uint, + set_int, + set_word, + set_byte, + set_usint, + set_sint, + set_time, +) +from snap7.util.setters import set_lreal + +logger = getLogger(__name__) + + +class DB: + """ + Manage a DB bytearray block given a specification + of the Layout. + + It is possible to have many repetitive instances of + a specification this is called a "row". + + Probably most usecases there is just one row + + Note: + This class has some of the semantics of a dict. In particular, the membership operators + (``in``, ``not it``), the access operator (``[]``), as well as the :func:`~DB.keys()` and + :func:`~DB.items()` methods work as usual. Iteration, on the other hand, happens on items + instead of keys (much like :func:`~DB.items()` method). + + Attributes: + bytearray_: buffer data from the PLC. + specification: layout of the DB Rows. + row_size: bytes size of a db row. + layout_offset: at which byte in the row specificaion we + start reading the data. + db_offset: at which byte in the db starts reading. + + Examples: + >>> db1[0]['testbool1'] = test + >>> db1.write(client) # puts data in plc + """ + + bytearray_: Optional[bytearray] = None # data from plc + specification: Optional[str] = None # layout of db rows + id_field: Optional[str] = None # ID field of the rows + row_size: int = 0 # bytes size of a db row + layout_offset: int = 0 # at which byte in row specification should + db_offset: int = 0 # at which byte in db should we start reading? + + # first fields could be be status data. + # and only the last part could be control data + # now you can be sure you will never overwrite + # critical parts of db + + def __init__( + self, + db_number: int, + bytearray_: bytearray, + specification: str, + row_size: int, + size: int, + id_field: Optional[str] = None, + db_offset: int = 0, + layout_offset: int = 0, + row_offset: int = 0, + area: Areas = Areas.DB, + ): + """Creates a new instance of the `Row` class. + + Args: + db_number: number of the DB to read from. This value should be 0 if area!=Areas.DB. + bytearray_: initial buffer read from the PLC. + specification: layout of the PLC memory. + row_size: bytes size of a db row. + size: lenght of the memory area. + id_field: name to reference the row. Optional. + db_offset: at which byte in the db starts reading. + layout_offset: at which byte in the row specificaion we + start reading the data. + row_offset: offset between rows. + area: which memory area this row is representing. + """ + self.db_number = db_number + self.size = size + self.row_size = row_size + self.id_field = id_field + self.area = area + + self.db_offset = db_offset + self.layout_offset = layout_offset + self.row_offset = row_offset + + self._bytearray = bytearray_ + self.specification = specification + # loop over bytearray. make rowObjects + # store index of id_field to row objects + self.index: OrderedDict = OrderedDict() + self.make_rows() + + def make_rows(self): + """Make each row for the DB.""" + id_field = self.id_field + row_size = self.row_size + specification = self.specification + layout_offset = self.layout_offset + row_offset = self.row_offset + + for i in range(self.size): + # calculate where row in bytearray starts + db_offset = i * (row_size + row_offset) + self.db_offset + # create a row object + row = DB_Row( + self, + specification, + row_size=row_size, + db_offset=db_offset, + layout_offset=layout_offset, + row_offset=self.row_offset, + area=self.area, + ) + + # store row object + key = row[id_field] if id_field else i + if key and key in self.index: + msg = f"{key} not unique!" + logger.error(msg) + self.index[key] = row + + def __getitem__(self, key: str, default: Optional[None] = None) -> Union[None, int, float, str, bool, datetime]: + """Access a row of the table through its index. + + Rows (values) are of type :class:`DB_Row`. + + Notes: + This method has the same semantics as :class:`dict` access. + """ + return self.index.get(key, default) + + def __iter__(self): + """Iterate over the items contained in the table, in the physical order they are contained + in memory. + + Notes: + This method does not have the same semantics as :class:`dict` iteration. Instead, it + has the same semantics as the :func:`~DB.items` method, yielding ``(index, row)`` + tuples. + """ + yield from self.index.items() + + def __len__(self): + """Return the number of rows contained in the DB. + + Notes: + If more than one row has the same index value, it is only counted once. + """ + return len(self.index) + + def __contains__(self, key): + """Return whether the given key is the index of a row in the DB.""" + return key in self.index + + def keys(self): + """Return a *view object* of the keys that are used as indices for the rows in the + DB. + """ + yield from self.index.keys() + + def items(self): + """Return a *view object* of the items (``(index, row)`` pairs) that are used as indices + for the rows in the DB. + """ + yield from self.index.items() + + def export(self): + """Export the object to an :class:`OrderedDict`, where each item in the dictionary + has an index as the key, and the value of the DB row associated with that index + as a value, represented itself as a :class:`dict` (as returned by :func:`DB_Row.export`). + + The outer dictionary contains the rows in the physical order they are contained in + memory. + + Notes: + This function effectively returns a snapshot of the DB. + """ + ret = OrderedDict() + for k, v in self.items(): + ret[k] = v.export() + return ret + + def set_data(self, bytearray_: bytearray): + """Set the new buffer data from the PLC to the current instance. + + Args: + bytearray_: buffer to save. + + Raises: + :obj:`TypeError`: if `bytearray_` is not an instance of :obj:`bytearray` + """ + if not isinstance(bytearray_, bytearray): + raise TypeError(f"Value bytearray_: {bytearray_} is not from type bytearray") + self._bytearray = bytearray_ + + def read(self, client: Client): + """Reads all the rows from the PLC to the :obj:`bytearray` of this instance. + + Args: + client: :obj:`Client` snap7 instance. + + Raises: + :obj:`ValueError`: if the `row_size` is less than 0. + """ + if self.row_size < 0: + raise ValueError("row_size must be greater equal zero.") + + total_size = self.size * (self.row_size + self.row_offset) + if self.area == Areas.DB: # note: is it worth using the upload method? + bytearray_ = client.db_read(self.db_number, self.db_offset, total_size) + else: + bytearray_ = client.read_area(self.area, 0, self.db_offset, total_size) + + # replace data in bytearray + for i, b in enumerate(bytearray_): + self._bytearray[i + self.db_offset] = b + + # todo: optimize by only rebuilding the index instead of all the DB_Row objects + self.index.clear() + self.make_rows() + + def write(self, client): + """Writes all the rows from the :obj:`bytearray` of this instance to the PLC + + Notes: + When the row_offset property has been set to something other than None while + constructing this object, this operation is not guaranteed to be atomic. + + Args: + client: :obj:`Client` snap7 instance. + + Raises: + :obj:`ValueError`: if the `row_size` is less than 0. + """ + if self.row_size < 0: + raise ValueError("row_size must be greater equal zero.") + + # special case: we have a row offset, so we must write each row individually + # this is because we don't want to change the data before the offset + if self.row_offset: + for _, v in self.index.items(): + v.write(client) + return + + total_size = self.size * (self.row_size + self.row_offset) + data = self._bytearray[self.db_offset : self.db_offset + total_size] + + if self.area == Areas.DB: + client.db_write(self.db_number, self.db_offset, data) + else: + client.write_area(self.area, 0, self.db_offset, data) + + +class DB_Row: + """ + Provide ROW API for DB bytearray + + Attributes: + bytearray_: reference to the data of the parent DB. + _specification: row specification layout. + """ + + bytearray_: bytearray # data of reference to parent DB + _specification: OrderedDict = OrderedDict() # row specification + + def __init__( + self, + bytearray_: bytearray, + _specification: str, + row_size: Optional[int] = 0, + db_offset: int = 0, + layout_offset: int = 0, + row_offset: Optional[int] = 0, + area: Optional[Areas] = Areas.DB, + ): + """Creates a new instance of the `DB_Row` class. + + Args: + bytearray_: reference to the data of the parent DB. + _specification: row specification layout. + row_size: Amount of bytes of the row. + db_offset: at which byte in the db starts reading. + layout_offset: at which byte in the row specificaion we + start reading the data. + row_offset: offset between rows. + area: which memory area this row is representing. + + Raises: + :obj:`TypeError`: if `bytearray_` is not an instance of :obj:`bytearray` or :obj:`DB`. + """ + + self.db_offset = db_offset # start point of row data in db + self.layout_offset = layout_offset # start point of row data in layout + self.row_size = row_size # lenght of the read + self.row_offset = row_offset # start of writable part of row + self.area = area + + if not isinstance(bytearray_, (bytearray, DB)): + raise TypeError(f"Value bytearray_ {bytearray_} is not from type (bytearray, DB)") + self._bytearray = bytearray_ + self._specification = parse_specification(_specification) + + def get_bytearray(self) -> bytearray: + """Gets bytearray from self or DB parent + + Returns: + Buffer data corresponding to the row. + """ + if isinstance(self._bytearray, DB): + return self._bytearray._bytearray + return self._bytearray + + def export(self) -> Dict[str, Union[str, int, float, bool, datetime]]: + """Export dictionary with values + + Returns: + dictionary containing the values of each value of the row. + """ + return {key: self[key] for key in self._specification} + + def __getitem__(self, key): + """ + Get a specific db field + """ + index, _type = self._specification[key] + return self.get_value(index, _type) + + def __setitem__(self, key, value): + index, _type = self._specification[key] + self.set_value(index, _type, value) + + def __repr__(self): + string = "" + for var_name, (index, _type) in self._specification.items(): + string = f"{string}\n{var_name:<20} {self.get_value(index, _type):<10}" + return string + + def unchanged(self, bytearray_: bytearray) -> bool: + """Checks if the bytearray is the same + + Args: + bytearray_: buffer of data to check. + + Returns: + True if the current `bytearray_` is equal to the new one. Otherwise is False. + """ + return self.get_bytearray() == bytearray_ + + def get_offset(self, byte_index: Union[str, int]) -> int: + """Calculate correct beginning position for a row + the db_offset = row_size * index + + Args: + byte_index: byte index from where to start reading from. + + Returns: + Amount of bytes to ignore. + """ + # add float typ to avoid error because of + # the variable address with decimal point(like 0.0 or 4.0) + return int(float(byte_index)) - self.layout_offset + self.db_offset + + def get_value(self, byte_index: Union[str, int], type_: str) -> Union[ValueError, int, float, str, datetime]: + """Gets the value for a specific type. + + Args: + byte_index: byte index from where start reading. + type_: type of data to read. + + Raises: + :obj:`ValueError`: if reading a `string` when checking the lenght of the string. + :obj:`ValueError`: if the `type_` is not handled. + + Returns: + Value read according to the `type_` + """ + bytearray_ = self.get_bytearray() + + # set parsing non case-sensitive + type_ = type_.upper() + + if type_ == "BOOL": + byte_index, bool_index = str(byte_index).split(".") + return get_bool(bytearray_, self.get_offset(byte_index), int(bool_index)) + + # remove 4 from byte index since + # first 4 bytes are used by db + byte_index = self.get_offset(byte_index) + + if type_.startswith("FSTRING"): + max_size = re.search(r"\d+", type_) + if max_size is None: + raise ValueError("Max size could not be determinate. re.search() returned None") + return get_fstring(bytearray_, byte_index, int(max_size[0])) + elif type_.startswith("STRING"): + max_size = re.search(r"\d+", type_) + if max_size is None: + raise ValueError("Max size could not be determinate. re.search() returned None") + return get_string(bytearray_, byte_index) + elif type_.startswith("WSTRING"): + max_size = re.search(r"\d+", type_) + if max_size is None: + raise ValueError("Max size could not be determinate. re.search() returned None") + return get_wstring(bytearray_, byte_index) + else: + type_to_func: Dict[str, Callable] = { + "REAL": get_real, + "DWORD": get_dword, + "UDINT": get_udint, + "DINT": get_dint, + "UINT": get_uint, + "INT": get_int, + "WORD": get_word, + "BYTE": get_byte, + "S5TIME": get_s5time, + "DATE_AND_TIME": get_dt, + "USINT": get_usint, + "SINT": get_sint, + "TIME": get_time, + "DATE": get_date, + "TIME_OF_DAY": get_tod, + "LREAL": get_lreal, + "TOD": get_tod, + "CHAR": get_char, + "WCHAR": get_wchar, + "DTL": get_dtl, + } + if type_ in type_to_func: + return type_to_func[type_](bytearray_, byte_index) + raise ValueError + + def set_value(self, byte_index: Union[str, int], type_: str, value: Union[bool, str, float]) -> Union[bytearray, None]: + """Sets the value for a specific type in the specified byte index. + + Args: + byte_index: byte index to start writing to. + type_: type of value to write. + value: value to write. + + Raises: + :obj:`ValueError`: if reading a `string` when checking the length of the string. + :obj:`ValueError`: if the `type_` is not handled. + + Returns: + Buffer data with the value written. Optional. + """ + + bytearray_ = self.get_bytearray() + + if type_ == "BOOL" and isinstance(value, bool): + byte_index, bool_index = str(byte_index).split(".") + return set_bool(bytearray_, self.get_offset(byte_index), int(bool_index), value) + + byte_index = self.get_offset(byte_index) + + if type_.startswith("FSTRING") and isinstance(value, str): + max_size = re.search(r"\d+", type_) + if max_size is None: + raise ValueError("Max size could not be determinate. re.search() returned None") + max_size_grouped = max_size.group(0) + max_size_int = int(max_size_grouped) + return set_fstring(bytearray_, byte_index, value, max_size_int) + + if type_.startswith("STRING") and isinstance(value, str): + max_size = re.search(r"\d+", type_) + if max_size is None: + raise ValueError("Max size could not be determinate. re.search() returned None") + max_size_grouped = max_size.group(0) + max_size_int = int(max_size_grouped) + return set_string(bytearray_, byte_index, value, max_size_int) + + if type_ == "REAL": + return set_real(bytearray_, byte_index, value) + + if type_ == "LREAL" and isinstance(value, float): + return set_lreal(bytearray_, byte_index, value) + + if isinstance(value, int): + type_to_func = { + "DWORD": set_dword, + "UDINT": set_udint, + "DINT": set_dint, + "UINT": set_uint, + "INT": set_int, + "WORD": set_word, + "BYTE": set_byte, + "USINT": set_usint, + "SINT": set_sint, + } + if type_ in type_to_func: + return type_to_func[type_](bytearray_, byte_index, value) + + if type_ == "TIME" and isinstance(value, str): + return set_time(bytearray_, byte_index, value) + + raise ValueError + + def write(self, client: Client) -> None: + """Write current data to db in plc + + Args: + client: :obj:`Client` snap7 instance. + + Raises: + :obj:`TypeError`: if the `_bytearray` is not an instance of :obj:`DB` class. + :obj:`ValueError`: if the `row_size` is less than 0. + """ + if not isinstance(self._bytearray, DB): + raise TypeError(f"Value self._bytearray: {self._bytearray} is not from type DB.") + if self.row_size < 0: + raise ValueError("row_size must be greater equal zero.") + + db_nr = self._bytearray.db_number + offset = self.db_offset + data = self.get_bytearray()[offset : offset + self.row_size] + db_offset = self.db_offset + + # indicate start of write only area of row! + if self.row_offset: + data = data[self.row_offset :] + db_offset += self.row_offset + + if self.area == Areas.DB: + client.db_write(db_nr, db_offset, data) + else: + client.write_area(self.area, 0, db_offset, data) + + def read(self, client: Client) -> None: + """Read current data of db row from plc. + + Args: + client: :obj:`Client` snap7 instance. + + Raises: + :obj:`TypeError`: if the `_bytearray` is not an instance of :obj:`DB` class. + :obj:`ValueError`: if the `row_size` is less than 0. + """ + if not isinstance(self._bytearray, DB): + raise TypeError(f"Value self._bytearray:{self._bytearray} is not from type DB.") + if self.row_size < 0: + raise ValueError("row_size must be greater equal zero.") + db_nr = self._bytearray.db_number + if self.area == Areas.DB: + bytearray_ = client.db_read(db_nr, self.db_offset, self.row_size) + else: + bytearray_ = client.read_area(self.area, 0, self.db_offset, self.row_size) + + data = self.get_bytearray() + # replace data in bytearray + for i, b in enumerate(bytearray_): + data[i + self.db_offset] = b diff --git a/snap7/util/getters.py b/snap7/util/getters.py new file mode 100644 index 00000000..f49cec17 --- /dev/null +++ b/snap7/util/getters.py @@ -0,0 +1,719 @@ +import struct +from datetime import timedelta, datetime, date +from typing import Union, List +from logging import getLogger + +logger = getLogger(__name__) + + +def get_bool(bytearray_: bytearray, byte_index: int, bool_index: int) -> bool: + """Get the boolean value from location in bytearray + + Args: + bytearray_: buffer data. + byte_index: byte index to read from. + bool_index: bit index to read from. + + Returns: + True if the bit is 1, else 0. + + Examples: + >>> buffer = bytearray([0b00000001]) # Only one byte length + >>> get_bool(buffer, 0, 0) # The bit 0 starts at the right. + True + """ + index_value = 1 << bool_index + byte_value = bytearray_[byte_index] + current_value = byte_value & index_value + return current_value == index_value + + +def get_byte(bytearray_: bytearray, byte_index: int) -> bytes: + """Get byte value from bytearray. + + Notes: + WORD 8bit 1bytes Decimal number unsigned B#(0) to B#(255) => 0 to 255 + + Args: + bytearray_: buffer to be read from. + byte_index: byte index to be read. + + Returns: + value get from the byte index. + """ + data = bytearray_[byte_index : byte_index + 1] + data[0] = data[0] & 0xFF + packed = struct.pack("B", *data) + value = struct.unpack("B", packed)[0] + return value + + +def get_word(bytearray_: bytearray, byte_index: int) -> bytearray: + """Get word value from bytearray. + + Notes: + WORD 16bit 2bytes Decimal number unsigned B#(0,0) to B#(255,255) => 0 to 65535 + + Args: + bytearray_: buffer to get the word from. + byte_index: byte index from where start reading from. + + Returns: + Word value. + + Examples: + >>> data = bytearray([0, 100]) # two bytes for a word + >>> snap7.util.get_word(data, 0) + 100 + """ + data = bytearray_[byte_index : byte_index + 2] + data[1] = data[1] & 0xFF + data[0] = data[0] & 0xFF + packed = struct.pack("2B", *data) + value = struct.unpack(">H", packed)[0] + return value + + +def get_int(bytearray_: bytearray, byte_index: int) -> int: + """Get int value from bytearray. + + Notes: + Datatype `int` in the PLC is represented in two bytes + + Args: + bytearray_: buffer to read from. + byte_index: byte index to start reading from. + + Returns: + Value read. + + Examples: + >>> data = bytearray([0, 255]) + >>> snap7.util.get_int(data, 0) + 255 + """ + data = bytearray_[byte_index : byte_index + 2] + data[1] = data[1] & 0xFF + data[0] = data[0] & 0xFF + packed = struct.pack("2B", *data) + value = struct.unpack(">h", packed)[0] + return value + + +def get_uint(bytearray_: bytearray, byte_index: int) -> int: + """Get unsigned int value from bytearray. + + Notes: + Datatype `uint` in the PLC is represented in two bytes + Maximum posible value is 65535. + Lower posible value is 0. + + Args: + bytearray_: buffer to read from. + byte_index: byte index to start reading from. + + Returns: + Value read. + + Examples: + >>> data = bytearray([255, 255]) + >>> snap7.util.get_uint(data, 0) + 65535 + """ + data = bytearray_[byte_index : byte_index + 2] + data[1] = data[1] & 0xFF + data[0] = data[0] & 0xFF + packed = struct.pack("2B", *data) + value = struct.unpack(">H", packed)[0] + return value + + +def get_real(bytearray_: bytearray, byte_index: int) -> float: + """Get real value. + + Notes: + Datatype `real` is represented in 4 bytes in the PLC. + The packed representation uses the `IEEE 754 binary32`. + + Args: + bytearray_: buffer to read from. + byte_index: byte index to reading from. + + Returns: + Real value. + + Examples: + >>> data = bytearray(b'B\\xf6\\xa4Z') + >>> snap7.util.get_real(data, 0) + 123.32099914550781 + """ + x = bytearray_[byte_index : byte_index + 4] + real = struct.unpack(">f", struct.pack("4B", *x))[0] + return real + + +def get_fstring(bytearray_: bytearray, byte_index: int, max_length: int, remove_padding: bool = True) -> str: + """Parse space-padded fixed-length string from bytearray + + Notes: + This function supports fixed-length ASCII strings, right-padded with spaces. + + Args: + bytearray_: buffer from where to get the string. + byte_index: byte index from where to start reading. + max_length: the maximum length of the string. + remove_padding: whether to remove the right-padding. + + Returns: + String value. + + Examples: + >>> data = [ord(letter) for letter in "hello world "] + >>> snap7.util.get_fstring(data, 0, 15) + 'hello world' + >>> snap7.util.get_fstring(data, 0, 15, remove_padding=false) + 'hello world ' + """ + data = map(chr, bytearray_[byte_index : byte_index + max_length]) + string = "".join(data) + + if remove_padding: + return string.rstrip(" ") + else: + return string + + +def get_string(bytearray_: bytearray, byte_index: int) -> str: + """Parse string from bytearray + + Notes: + The first byte of the buffer will contain the max size posible for a string. + The second byte contains the length of the string that contains. + + Args: + bytearray_: buffer from where to get the string. + byte_index: byte index from where to start reading. + + Returns: + String value. + + Examples: + >>> data = bytearray([254, len("hello world")] + [ord(letter) for letter in "hello world"]) + >>> snap7.util.get_string(data, 0) + 'hello world' + """ + + str_length = int(bytearray_[byte_index + 1]) + max_string_size = int(bytearray_[byte_index]) + + if str_length > max_string_size or max_string_size > 254: + logger.error("The string is too big for the size encountered in specification") + logger.error("WRONG SIZED STRING ENCOUNTERED") + raise TypeError( + "String contains {str_length} chars, but max. {max_string_size} chars are expected or is " + "larger than 254. Bytearray doesn't seem to be a valid string." + ) + data = map(chr, bytearray_[byte_index + 2 : byte_index + 2 + str_length]) + return "".join(data) + + +def get_dword(bytearray_: bytearray, byte_index: int) -> int: + """Gets the dword from the buffer. + + Notes: + Datatype `dword` consists in 8 bytes in the PLC. + The maximum value posible is `4294967295` + + Args: + bytearray_: buffer to read. + byte_index: byte index from where to start reading. + + Returns: + Value read. + + Examples: + >>> data = bytearray(8) + >>> data[:] = b"\\x12\\x34\\xAB\\xCD" + >>> snap7.util.get_dword(data, 0) + 4294967295 + """ + data = bytearray_[byte_index : byte_index + 4] + dword = struct.unpack(">I", struct.pack("4B", *data))[0] + return dword + + +def get_dint(bytearray_: bytearray, byte_index: int) -> int: + """Get dint value from bytearray. + + Notes: + Datatype `dint` consists in 4 bytes in the PLC. + Maximum possible value is 2147483647. + Lower posible value is -2147483648. + + Args: + bytearray_: buffer to read. + byte_index: byte index from where to start reading. + + Returns: + Value read. + + Examples: + >>> import struct + >>> data = bytearray(4) + >>> data[:] = struct.pack(">i", 2147483647) + >>> snap7.util.get_dint(data, 0) + 2147483647 + """ + data = bytearray_[byte_index : byte_index + 4] + dint = struct.unpack(">i", struct.pack("4B", *data))[0] + return dint + + +def get_udint(bytearray_: bytearray, byte_index: int) -> int: + """Get unsigned dint value from bytearray. + + Notes: + Datatype `udint` consists in 4 bytes in the PLC. + Maximum possible value is 4294967295. + Minimum posible value is 0. + + Args: + bytearray_: buffer to read. + byte_index: byte index from where to start reading. + + Returns: + Value read. + + Examples: + >>> import struct + >>> data = bytearray(4) + >>> data[:] = struct.pack(">I", 4294967295) + >>> snap7.util.get_udint(data, 0) + 4294967295 + """ + data = bytearray_[byte_index : byte_index + 4] + dint = struct.unpack(">I", struct.pack("4B", *data))[0] + return dint + + +def get_s5time(bytearray_: bytearray, byte_index: int) -> str: + micro_to_milli = 1000 + data_bytearray = bytearray_[byte_index : byte_index + 2] + s5time_data_int_like = list(data_bytearray.hex()) + if s5time_data_int_like[0] == "0": + # 10ms + time_base = 10 + elif s5time_data_int_like[0] == "1": + # 100ms + time_base = 100 + elif s5time_data_int_like[0] == "2": + # 1s + time_base = 1000 + elif s5time_data_int_like[0] == "3": + # 10s + time_base = 10000 + else: + raise ValueError("This value should not be greater than 3") + + s5time_bcd = int(s5time_data_int_like[1]) * 100 + int(s5time_data_int_like[2]) * 10 + int(s5time_data_int_like[3]) + s5time_microseconds = time_base * s5time_bcd + s5time = timedelta(microseconds=s5time_microseconds * micro_to_milli) + # here we must return a string like variable, otherwise nothing will return + return "".join(str(s5time)) + + +def get_dt(bytearray_: bytearray, byte_index: int) -> str: + """Get DATE_AND_TIME Value from bytearray as ISO 8601 formatted Date String + Notes: + Datatype `DATE_AND_TIME` consists in 8 bytes in the PLC. + Args: + bytearray_: buffer to read. + byte_index: byte index from where to start writing. + Examples: + >>> data = bytearray(8) + >>> data[:] = [32, 7, 18, 23, 50, 2, 133, 65] #'2020-07-12T17:32:02.854000' + >>> get_dt(data,0) + '2020-07-12T17:32:02.854000' + """ + return get_date_time_object(bytearray_, byte_index).isoformat(timespec="microseconds") + + +def get_date_time_object(bytearray_: bytearray, byte_index: int) -> datetime: + """Get DATE_AND_TIME Value from bytearray as python datetime object + Notes: + Datatype `DATE_AND_TIME` consists in 8 bytes in the PLC. + Args: + bytearray_: buffer to read. + byte_index: byte index from where to start writing. + Examples: + >>> data = bytearray(8) + >>> data[:] = [32, 7, 18, 23, 50, 2, 133, 65] #date '2020-07-12 17:32:02.854' + >>> get_date_time_object(data,0) + datetime.datetime(2020, 7, 12, 17, 32, 2, 854000) + """ + + def bcd_to_byte(byte: int) -> int: + return (byte >> 4) * 10 + (byte & 0xF) + + year = bcd_to_byte(bytearray_[byte_index]) + # between 1990 and 2089, only last two digits are saved in DB 90 - 89 + year = 2000 + year if year < 90 else 1900 + year + month = bcd_to_byte(bytearray_[byte_index + 1]) + day = bcd_to_byte(bytearray_[byte_index + 2]) + hour = bcd_to_byte(bytearray_[byte_index + 3]) + min_ = bcd_to_byte(bytearray_[byte_index + 4]) + sec = bcd_to_byte(bytearray_[byte_index + 5]) + # plc save miliseconds in two bytes with the most signifanct byte used only + # in the last byte for microseconds the other for weekday + # * 1000 because pythoin datetime needs microseconds not milli + microsec = (bcd_to_byte(bytearray_[byte_index + 6]) * 10 + bcd_to_byte(bytearray_[byte_index + 7] >> 4)) * 1000 + + return datetime(year, month, day, hour, min_, sec, microsec) + + +def get_time(bytearray_: bytearray, byte_index: int) -> str: + """Get time value from bytearray. + + Notes: + Datatype `time` consists in 4 bytes in the PLC. + Maximum possible value is T#24D_20H_31M_23S_647MS(2147483647). + Lower posible value is T#-24D_20H_31M_23S_648MS(-2147483648). + + Args: + bytearray_: buffer to read. + byte_index: byte index from where to start reading. + + Returns: + Value read. + + Examples: + >>> import struct + >>> data = bytearray(4) + >>> data[:] = struct.pack(">i", 2147483647) + >>> snap7.util.get_time(data, 0) + '24:20:31:23:647' + """ + data_bytearray = bytearray_[byte_index : byte_index + 4] + bits = 32 + sign = 1 + byte_str = data_bytearray.hex() + val = int(byte_str, 16) + if (val & (1 << (bits - 1))) != 0: + sign = -1 # if sign bit is set e.g., 8bit: 128-255 + val -= 1 << bits # compute negative value + val *= sign + + milli_seconds = val % 1000 + seconds = val // 1000 + minutes = seconds // 60 + hours = minutes // 60 + days = hours // 24 + + sign_str = "" if sign >= 0 else "-" + time_str = f"{sign_str}{days!s}:{hours % 24!s}:{minutes % 60!s}:{seconds % 60!s}.{milli_seconds!s}" + + return time_str + + +def get_usint(bytearray_: bytearray, byte_index: int) -> int: + """Get the unsigned small int from the bytearray + + Notes: + Datatype `usint` (Unsigned small int) consists on 1 byte in the PLC. + Maximum posible value is 255. + Lower posible value is 0. + + Args: + bytearray_: buffer to read from. + byte_index: byte index from where to start reading. + + Returns: + Value read. + + Examples: + >>> data = bytearray([255]) + >>> snap7.util.get_usint(data, 0) + 255 + """ + data = bytearray_[byte_index] & 0xFF + packed = struct.pack("B", data) + value = struct.unpack(">B", packed)[0] + return value + + +def get_sint(bytearray_: bytearray, byte_index: int) -> int: + """Get the small int + + Notes: + Datatype `sint` (Small int) consists in 1 byte in the PLC. + Maximum value posible is 127. + Lowest value posible is -128. + + Args: + bytearray_: buffer to read from. + byte_index: byte index from where to start reading. + + Returns: + Value read. + + Examples: + >>> data = bytearray([127]) + >>> snap7.util.get_sint(data, 0) + 127 + """ + data = bytearray_[byte_index] + packed = struct.pack("B", data) + value = struct.unpack(">b", packed)[0] + return value + + +def get_lint(bytearray_: bytearray, byte_index: int): + """Get the long int + + THIS VALUE IS NEITHER TESTED NOR VERIFIED BY A REAL PLC AT THE MOMENT + + Notes: + Datatype `lint` (long int) consists in 8 bytes in the PLC. + Maximum value posible is +9223372036854775807 + Lowest value posible is -9223372036854775808 + + Args: + bytearray_: buffer to read from. + byte_index: byte index from where to start reading. + + Returns: + Value read. + + Examples: + read lint value (here as example 12345) from DB1.10 of a PLC + >>> data = client.db_read(db_number=1, start=10, size=8) + >>> snap7.util.get_lint(data, 0) + 12345 + """ + + # raw_lint = bytearray_[byte_index:byte_index + 8] + # lint = struct.unpack('>q', struct.pack('8B', *raw_lint))[0] + # return lint + return NotImplementedError + + +def get_lreal(bytearray_: bytearray, byte_index: int) -> float: + """Get the long real + + Notes: + Datatype `lreal` (long real) consists in 8 bytes in the PLC. + Negative Range: -1.7976931348623158e+308 to -2.2250738585072014e-308 + Positive Range: +2.2250738585072014e-308 to +1.7976931348623158e+308 + Zero: ±0 + + Args: + bytearray_: buffer to read from. + byte_index: byte index from where to start reading. + + Returns: + Value read. + + Examples: + read lreal value (here as example 12345.12345) from DB1.10 of a PLC + >>> data = client.db_read(db_number=1, start=10, size=8) + >>> snap7.util.get_lreal(data, 0) + 12345.12345 + """ + return struct.unpack_from(">d", bytearray_, offset=byte_index)[0] + + +def get_lword(bytearray_: bytearray, byte_index: int) -> bytearray: + """Get the long word + + THIS VALUE IS NEITHER TESTED NOR VERIFIED BY A REAL PLC AT THE MOMENT + + Notes: + Datatype `lword` (long word) consists in 8 bytes in the PLC. + Maximum value posible is bytearray(b"\\xFF\\xFF\\xFF\\xFF\\xFF\\xFF\\xFF\\xFF") + Lowest value posible is bytearray(b"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00") + + Args: + bytearray_: buffer to read from. + byte_index: byte index from where to start reading. + + Returns: + Value read. + + Examples: + read lword value (here as example 0xAB\0xCD) from DB1.10 of a PLC + >>> data = client.db_read(db_number=1, start=10, size=8) + >>> snap7.util.get_lword(data, 0) + bytearray(b"\\x00\\x00\\x00\\x00\\x00\\x00\\xAB\\xCD") + """ + # data = bytearray_[byte_index:byte_index + 4] + # dword = struct.unpack('>Q', struct.pack('8B', *data))[0] + # return bytearray(dword) + raise NotImplementedError + + +def get_ulint(bytearray_: bytearray, byte_index: int) -> int: + """Get ulint value from bytearray. + + Notes: + Datatype `int` in the PLC is represented in 8 bytes + + Args: + bytearray_: buffer to read from. + byte_index: byte index to start reading from. + + Returns: + Value read. + + Examples: + Read 8 Bytes raw from DB1.10, where an ulint value is stored. Return Python compatible value. + >>> data = client.db_read(db_number=1, start=10, size=8) + >>> snap7.util.get_ulint(data, 0) + 12345 + """ + raw_ulint = bytearray_[byte_index : byte_index + 8] + lint = struct.unpack(">Q", struct.pack("8B", *raw_ulint))[0] + return lint + + +def get_tod(bytearray_: bytearray, byte_index: int) -> timedelta: + len_bytearray_ = len(bytearray_) + byte_range = byte_index + 4 + if len_bytearray_ < byte_range: + raise ValueError("Date can't be extracted from bytearray. bytearray_[Index:Index+16] would cause overflow.") + time_val = timedelta(milliseconds=int.from_bytes(bytearray_[byte_index:byte_range], byteorder="big")) + if time_val.days >= 1: + raise ValueError("Time_Of_Date can't be extracted from bytearray. Bytearray contains unexpected values.") + return time_val + + +def get_date(bytearray_: bytearray, byte_index: int = 0) -> date: + len_bytearray_ = len(bytearray_) + byte_range = byte_index + 2 + if len_bytearray_ < byte_range: + raise ValueError("Date can't be extracted from bytearray. bytearray_[Index:Index+16] would cause overflow.") + date_val = date(1990, 1, 1) + timedelta(days=int.from_bytes(bytearray_[byte_index:byte_range], byteorder="big")) + if date_val > date(2168, 12, 31): + raise ValueError("date_val is higher than specification allows.") + return date_val + + +def get_ltime(bytearray_: bytearray, byte_index: int) -> str: + raise NotImplementedError + + +def get_ltod(bytearray_: bytearray, byte_index: int) -> str: + raise NotImplementedError + + +def get_ldt(bytearray_: bytearray, byte_index: int) -> str: + raise NotImplementedError + + +def get_dtl(bytearray_: bytearray, byte_index: int) -> datetime: + time_to_datetime = datetime( + year=int.from_bytes(bytearray_[byte_index : byte_index + 2], byteorder="big"), + month=int(bytearray_[byte_index + 2]), + day=int(bytearray_[byte_index + 3]), + hour=int(bytearray_[byte_index + 5]), + minute=int(bytearray_[byte_index + 6]), + second=int(bytearray_[byte_index + 7]), + microsecond=int(bytearray_[byte_index + 8]), + ) # --- ? noch nicht genau genug + if time_to_datetime > datetime(2554, 12, 31, 23, 59, 59): + raise ValueError("date_val is higher than specification allows.") + return time_to_datetime + + +def get_char(bytearray_: bytearray, byte_index: int) -> str: + """Get char value from bytearray. + + Notes: + Datatype `char` in the PLC is represented in 1 byte. It has to be in ASCII-format. + + Args: + bytearray_: buffer to read from. + byte_index: byte index to start reading from. + + Returns: + Value read. + + Examples: + Read 1 Byte raw from DB1.10, where a char value is stored. Return Python compatible value. + >>> data = client.db_read(db_number=1, start=10, size=1) + >>> snap7.util.get_char(data, 0) + 'C' + """ + char = chr(bytearray_[byte_index]) + return char + + +def get_wchar(bytearray_: bytearray, byte_index: int) -> Union[ValueError, str]: + """Get wchar value from bytearray. + + Notes: + Datatype `wchar` in the PLC is represented in 2 bytes. It has to be in utf-16-be format. + + + Args: + bytearray_: buffer to read from. + byte_index: byte index to start reading from. + + Returns: + Value read. + + Examples: + Read 2 Bytes raw from DB1.10, where a wchar value is stored. Return Python compatible value. + >>> data = client.db_read(db_number=1, start=10, size=2) + >>> snap7.util.get_wchar(data, 0) + 'C' + """ + if bytearray_[byte_index] == 0: + return chr(bytearray_[1]) + return bytearray_[byte_index : byte_index + 2].decode("utf-16-be") + + +def get_wstring(bytearray_: bytearray, byte_index: int) -> str: + """Parse wstring from bytearray + + Notes: + Byte 0 and 1 contains the max size posible for a string (2 Byte value). + byte 2 and 3 contains the length of the string that contains (2 Byte value). + The other bytes contain WCHARs (2Byte) in utf-16-be style. + + Args: + bytearray_: buffer from where to get the string. + byte_index: byte index from where to start reading. + + Returns: + String value. + + Examples: + Read from DB1.10 22, where the WSTRING is stored, the raw 22 Bytes and convert them to a python string + >>> data = client.db_read(db_number=1, start=10, size=22) + >>> snap7.util.get_wstring(data, 0) + 'hello world' + """ + # Byte 0 + 1 --> total length of wstring, should be bytearray_ - 4 + # Byte 2, 3 --> used length of wstring + wstring_start = byte_index + 4 + + max_wstring_size = bytearray_[byte_index : byte_index + 2] + packed = struct.pack("2B", *max_wstring_size) + max_wstring_symbols = struct.unpack(">H", packed)[0] * 2 + + wstr_length_raw = bytearray_[byte_index + 2 : byte_index + 4] + wstr_symbols_amount = struct.unpack(">H", struct.pack("2B", *wstr_length_raw))[0] * 2 + + if wstr_symbols_amount > max_wstring_symbols or max_wstring_symbols > 16382: + logger.error("The wstring is too big for the size encountered in specification") + logger.error("WRONG SIZED STRING ENCOUNTERED") + raise TypeError( + f"WString contains {wstr_symbols_amount} chars, but max {max_wstring_symbols} chars are " + f"expected or is larger than 16382. Bytearray doesn't seem to be a valid string." + ) + + return bytearray_[wstring_start : wstring_start + wstr_symbols_amount].decode("utf-16-be") + + +def get_array(bytearray_: bytearray, byte_index: int) -> List: + raise NotImplementedError diff --git a/snap7/util/setters.py b/snap7/util/setters.py new file mode 100644 index 00000000..dffc96e7 --- /dev/null +++ b/snap7/util/setters.py @@ -0,0 +1,510 @@ +import re +import struct +from typing import Union + +from .getters import get_bool + + +def set_bool(bytearray_: bytearray, byte_index: int, bool_index: int, value: bool) -> bytearray: + """Set boolean value on location in bytearray. + + Args: + bytearray_: buffer to write to. + byte_index: byte index to write to. + bool_index: bit index to write to. + value: value to write. + + Examples: + >>> buffer = bytearray([0b00000000]) + >>> set_bool(buffer, 0, 0, True) + >>> buffer + bytearray(b"\\x01") + """ + if value not in {0, 1, True, False}: + raise TypeError(f"Value value:{value} is not a boolean expression.") + + current_value = get_bool(bytearray_, byte_index, bool_index) + index_value = 1 << bool_index + + # check if bool already has correct value + if current_value == value: + return bytearray_ + + if value: + # make sure index_v is IN current byte + bytearray_[byte_index] += index_value + else: + # make sure index_v is NOT in current byte + bytearray_[byte_index] -= index_value + return bytearray_ + + +def set_byte(bytearray_: bytearray, byte_index: int, _int: int) -> bytearray: + """Set value in bytearray to byte + + Args: + bytearray_: buffer to write to. + byte_index: byte index to write. + _int: value to write. + + Returns: + buffer with the written value. + + Examples: + >>> buffer = bytearray([0b00000000]) + >>> set_byte(buffer, 0, 255) + bytearray(b"\\xFF") + """ + _int = int(_int) + _bytes = struct.pack("B", _int) + bytearray_[byte_index : byte_index + 1] = _bytes + return bytearray_ + + +def set_word(bytearray_: bytearray, byte_index: int, _int: int): + """Set value in bytearray to word + + Notes: + Word datatype is 2 bytes long. + + Args: + bytearray_: buffer to be written. + byte_index: byte index to start write from. + _int: value to be write. + + Return: + buffer with the written value + """ + _int = int(_int) + _bytes = struct.unpack("2B", struct.pack(">H", _int)) + bytearray_[byte_index : byte_index + 2] = _bytes + return bytearray_ + + +def set_int(bytearray_: bytearray, byte_index: int, _int: int): + """Set value in bytearray to int + + Notes: + An datatype `int` in the PLC consists of two `bytes`. + + Args: + bytearray_: buffer to write on. + byte_index: byte index to start writing from. + _int: int value to write. + + Returns: + Buffer with the written value. + + Examples: + >>> data = bytearray(2) + >>> snap7.util.set_int(data, 0, 255) + bytearray(b'\\x00\\xff') + """ + # make sure were dealing with an int + _int = int(_int) + _bytes = struct.unpack("2B", struct.pack(">h", _int)) + bytearray_[byte_index : byte_index + 2] = _bytes + return bytearray_ + + +def set_uint(bytearray_: bytearray, byte_index: int, _int: int): + """Set value in bytearray to unsigned int + + Notes: + An datatype `uint` in the PLC consists of two `bytes`. + + Args: + bytearray_: buffer to write on. + byte_index: byte index to start writing from. + _int: int value to write. + + Returns: + Buffer with the written value. + + Examples: + >>> data = bytearray(2) + >>> snap7.util.set_uint(data, 0, 65535) + bytearray(b'\\xff\\xff') + """ + # make sure were dealing with an int + _int = int(_int) + _bytes = struct.unpack("2B", struct.pack(">H", _int)) + bytearray_[byte_index : byte_index + 2] = _bytes + return bytearray_ + + +def set_real(bytearray_: bytearray, byte_index: int, real) -> bytearray: + """Set Real value + + Notes: + Datatype `real` is represented in 4 bytes in the PLC. + The packed representation uses the `IEEE 754 binary32`. + + Args: + bytearray_: buffer to write to. + byte_index: byte index to start writing from. + real: value to be written. + + Returns: + Buffer with the value written. + + Examples: + >>> data = bytearray(4) + >>> snap7.util.set_real(data, 0, 123.321) + bytearray(b'B\\xf6\\xa4Z') + """ + real = float(real) + real = struct.pack(">f", real) + _bytes = struct.unpack("4B", real) + for i, b in enumerate(_bytes): + bytearray_[byte_index + i] = b + return bytearray_ + + +def set_fstring(bytearray_: bytearray, byte_index: int, value: str, max_length: int): + """Set space-padded fixed-length string value + + Args: + bytearray_: buffer to write to. + byte_index: byte index to start writing from. + value: string to write. + max_length: maximum string length, i.e. the fixed size of the string. + + Raises: + :obj:`TypeError`: if the `value` is not a :obj:`str`. + :obj:`ValueError`: if the length of the `value` is larger than the `max_size` + or 'value' contains non-ascii characters. + + Examples: + >>> data = bytearray(20) + >>> snap7.util.set_fstring(data, 0, "hello world", 15) + >>> data + bytearray(b'hello world \x00\x00\x00\x00\x00') + """ + if not value.isascii(): + raise ValueError("Value contains non-ascii values.") + # FAIL HARD WHEN trying to write too much data into PLC + size = len(value) + if size > max_length: + raise ValueError(f"size {size} > max_length {max_length} {value}") + + i = 0 + + # fill array which chr integers + for i, c in enumerate(value): + bytearray_[byte_index + i] = ord(c) + + # fill the rest with empty space + for r in range(i + 1, max_length): + bytearray_[byte_index + r] = ord(" ") + + +def set_string(bytearray_: bytearray, byte_index: int, value: str, max_size: int = 254): + """Set string value + + Args: + bytearray_: buffer to write to. + byte_index: byte index to start writing from. + value: string to write. + max_size: maximum possible string size, max. 254 as default. + + Raises: + :obj:`TypeError`: if the `value` is not a :obj:`str`. + :obj:`ValueError`: if the length of the `value` is larger than the `max_size` + or 'max_size' is greater than 254 or 'value' contains non-ascii characters. + + Examples: + >>> data = bytearray(20) + >>> snap7.util.set_string(data, 0, "hello world", 254) + >>> data + bytearray(b'\\xff\\x0bhello world\\x00\\x00\\x00\\x00\\x00\\x00\\x00') + """ + if not isinstance(value, str): + raise TypeError(f"Value value:{value} is not from Type string") + + if max_size > 254: + raise ValueError(f"max_size: {max_size} > max. allowed 254 chars") + if not value.isascii(): + raise ValueError( + "Value contains non-ascii values, which is not compatible with PLC Type STRING." + "Check encoding of value or try set_wstring() (utf-16 encoding needed)." + ) + size = len(value) + # FAIL HARD WHEN trying to write too much data into PLC + if size > max_size: + raise ValueError(f"size {size} > max_size {max_size} {value}") + + # set max string size + bytearray_[byte_index] = max_size + + # set len count on first position + bytearray_[byte_index + 1] = len(value) + + i = 0 + + # fill array which chr integers + for i, c in enumerate(value): + bytearray_[byte_index + 2 + i] = ord(c) + + # fill the rest with empty space + for r in range(i + 1, bytearray_[byte_index] - 2): + bytearray_[byte_index + 2 + r] = ord(" ") + + +def set_dword(bytearray_: bytearray, byte_index: int, dword: int): + """Set a DWORD to the buffer. + + Notes: + Datatype `dword` consists in 8 bytes in the PLC. + The maximum value posible is `4294967295` + + Args: + bytearray_: buffer to write to. + byte_index: byte index from where to writing reading. + dword: value to write. + + Examples: + >>> data = bytearray(4) + >>> snap7.util.set_dword(data,0, 4294967295) + >>> data + bytearray(b'\\xff\\xff\\xff\\xff') + """ + dword = int(dword) + _bytes = struct.unpack("4B", struct.pack(">I", dword)) + for i, b in enumerate(_bytes): + bytearray_[byte_index + i] = b + + +def set_dint(bytearray_: bytearray, byte_index: int, dint: int): + """Set value in bytearray to dint + + Notes: + Datatype `dint` consists in 4 bytes in the PLC. + Maximum possible value is 2147483647. + Lower posible value is -2147483648. + + Args: + bytearray_: buffer to write. + byte_index: byte index from where to start writing. + dint: double integer value + + Examples: + >>> data = bytearray(4) + >>> snap7.util.set_dint(data, 0, 2147483647) + >>> data + bytearray(b'\\x7f\\xff\\xff\\xff') + """ + dint = int(dint) + _bytes = struct.unpack("4B", struct.pack(">i", dint)) + for i, b in enumerate(_bytes): + bytearray_[byte_index + i] = b + + +def set_udint(bytearray_: bytearray, byte_index: int, udint: int): + """Set value in bytearray to unsigned dint + + Notes: + Datatype `dint` consists in 4 bytes in the PLC. + Maximum possible value is 4294967295. + Minimum posible value is 0. + + Args: + bytearray_: buffer to write. + byte_index: byte index from where to start writing. + udint: unsigned double integer value + + Examples: + >>> data = bytearray(4) + >>> snap7.util.set_udint(data, 0, 4294967295) + >>> data + bytearray(b'\\xff\\xff\\xff\\xff') + """ + udint = int(udint) + _bytes = struct.unpack("4B", struct.pack(">I", udint)) + for i, b in enumerate(_bytes): + bytearray_[byte_index + i] = b + + +def set_time(bytearray_: bytearray, byte_index: int, time_string: str) -> bytearray: + """Set value in bytearray to time + + Notes: + Datatype `time` consists in 4 bytes in the PLC. + Maximum possible value is T#24D_20H_31M_23S_647MS(2147483647). + Lower posible value is T#-24D_20H_31M_23S_648MS(-2147483648). + + Args: + bytearray_: buffer to write. + byte_index: byte index from where to start writing. + time_string: time value in string + + Examples: + >>> data = bytearray(4) + + >>> snap7.util.set_time(data, 0, '-22:3:57:28.192') + + >>> data + bytearray(b'\x8d\xda\xaf\x00') + """ + sign = 1 + if re.fullmatch( + r"(-?(2[0-3]|1?\d):(2[0-3]|1?\d|\d):([1-5]?\d):[1-5]?\d.\d{1,3})|" + r"(-24:(20|1?\d):(3[0-1]|[0-2]?\d):(2[0-3]|1?\d).(64[0-8]|6[0-3]\d|[0-5]\d{1,2}))|" + r"(24:(20|1?\d):(3[0-1]|[0-2]?\d):(2[0-3]|1?\d).(64[0-7]|6[0-3]\d|[0-5]\d{1,2}))", + time_string, + ): + data_list = re.split("[: .]", time_string) + days: str = data_list[0] + hours: int = int(data_list[1]) + minutes: int = int(data_list[2]) + seconds: int = int(data_list[3]) + milli_seconds: int = int(data_list[4].ljust(3, "0")) + if re.match(r"^-\d{1,2}$", days): + sign = -1 + + time_int = ( + (int(days) * sign * 3600 * 24 + (hours % 24) * 3600 + (minutes % 60) * 60 + seconds % 60) * 1000 + milli_seconds + ) * sign + bytes_array = time_int.to_bytes(4, byteorder="big", signed=True) + bytearray_[byte_index : byte_index + 4] = bytes_array + return bytearray_ + else: + raise ValueError("time value out of range, please check the value interval") + + +def set_usint(bytearray_: bytearray, byte_index: int, _int: int) -> bytearray: + """Set unsigned small int + + Notes: + Datatype `usint` (Unsigned small int) consists on 1 byte in the PLC. + Maximum posible value is 255. + Lower posible value is 0. + + Args: + bytearray_: buffer to write. + byte_index: byte index from where to start writing. + _int: value to write. + + Returns: + Buffer with the written value. + + Examples: + >>> data = bytearray(1) + >>> snap7.util.set_usint(data, 0, 255) + bytearray(b'\\xff') + """ + _int = int(_int) + _bytes = struct.unpack("B", struct.pack(">B", _int)) + bytearray_[byte_index] = _bytes[0] + return bytearray_ + + +def set_sint(bytearray_: bytearray, byte_index: int, _int) -> bytearray: + """Set small int to the buffer. + + Notes: + Datatype `sint` (Small int) consists in 1 byte in the PLC. + Maximum value posible is 127. + Lowest value posible is -128. + + Args: + bytearray_: buffer to write to. + byte_index: byte index from where to start writing. + _int: value to write. + + Returns: + Buffer with the written value. + + Examples: + >>> data = bytearray(1) + >>> snap7.util.set_sint(data, 0, 127) + bytearray(b'\\x7f') + """ + _int = int(_int) + _bytes = struct.unpack("B", struct.pack(">b", _int)) + bytearray_[byte_index] = _bytes[0] + return bytearray_ + + +def set_lreal(bytearray_: bytearray, byte_index: int, lreal: float) -> bytearray: + """Set the long real + + Notes: + Datatype `lreal` (long real) consists in 8 bytes in the PLC. + Negative Range: -1.7976931348623158e+308 to -2.2250738585072014e-308 + Positive Range: +2.2250738585072014e-308 to +1.7976931348623158e+308 + Zero: ±0 + + Args: + bytearray_: buffer to read from. + byte_index: byte index from where to start reading. + lreal: float value to set + + Returns: + Value to write. + + Examples: + write lreal value (here as example 12345.12345) to DB1.10 of a PLC + >>> data = snap7.util.set_lreal(data, 12345.12345) + >>> client.db_write(db_number=1, start=10, data=data) + + """ + lreal = float(lreal) + struct.pack_into(">d", bytearray_, byte_index, lreal) + return bytearray_ + + +def set_lword(bytearray_: bytearray, byte_index: int, lword: bytearray) -> bytearray: + """Set the long word + + THIS VALUE IS NEITHER TESTED NOR VERIFIED BY A REAL PLC AT THE MOMENT + + Notes: + Datatype `lword` (long word) consists in 8 bytes in the PLC. + Maximum value posible is bytearray(b"\\xFF\\xFF\\xFF\\xFF\\xFF\\xFF\\xFF\\xFF") + Lowest value posible is bytearray(b"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00") + + Args: + bytearray_: buffer to read from. + byte_index: byte index from where to start reading. + lword: Value to write + + Returns: + Bytearray conform value. + + Examples: + read lword value (here as example 0xAB\0xCD) from DB1.10 of a PLC + >>> data = snap7.util.set_lword(data, 0, bytearray(b"\\x00\\x00\\x00\\x00\\x00\\x00\\xAB\\xCD")) + bytearray(b"\\x00\\x00\\x00\\x00\\x00\\x00\\xAB\\xCD") + >>> client.db_write(db_number=1, start=10, data=data) + """ + # data = bytearray_[byte_index:byte_index + 4] + # dword = struct.unpack('8B', struct.pack('>Q', *data))[0] + # return bytearray(dword) + raise NotImplementedError + + +def set_char(bytearray_: bytearray, byte_index: int, chr_: str) -> Union[ValueError, bytearray]: + """Set char value in a bytearray. + + Notes: + Datatype `char` in the PLC is represented in 1 byte. It has to be in ASCII-format + + Args: + bytearray_: buffer to read from. + byte_index: byte index to start reading from. + chr_: Char to be set + + Returns: + Value read. + + Examples: + Read 1 Byte raw from DB1.10, where a char value is stored. Return Python compatible value. + >>> data = snap7.util.set_char(data, 0, 'C') + >>> client.db_write(db_number=1, start=10, data=data) + 'bytearray('0x43') + """ + if chr_.isascii(): + bytearray_[byte_index] = ord(chr_) + return bytearray_ + raise ValueError(f"chr_ : {chr_} contains a None-Ascii value, but ASCII-only is allowed.") diff --git a/tests/bla.py b/tests/bla.py new file mode 100644 index 00000000..b879d31c --- /dev/null +++ b/tests/bla.py @@ -0,0 +1,3 @@ +from snap7.server import mainloop + +mainloop(1102, True) diff --git a/tests/test_client.py b/tests/test_client.py index aee73db4..6f664e20 100755 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -12,6 +12,8 @@ import snap7 +import snap7.util.getters +import snap7.util.setters from snap7 import util from snap7.common import check_error from snap7.server import mainloop @@ -20,7 +22,7 @@ logging.basicConfig(level=logging.WARNING) -ip = '127.0.0.1' +ip = "127.0.0.1" tcpport = 1102 db_number = 1 rack = 1 @@ -29,7 +31,6 @@ @pytest.mark.client class TestClient(unittest.TestCase): - process = None @classmethod @@ -87,16 +88,16 @@ def test_read_multi_vars(self): # build and write test values test_value_1 = 129.5 - test_bytes_1 = bytearray(struct.pack('>f', test_value_1)) + test_bytes_1 = bytearray(struct.pack(">f", test_value_1)) self.client.db_write(db, 0, test_bytes_1) test_value_2 = -129.5 - test_bytes_2 = bytearray(struct.pack('>f', test_value_2)) + test_bytes_2 = bytearray(struct.pack(">f", test_value_2)) self.client.db_write(db, 4, test_bytes_2) test_value_3 = 123 test_bytes_3 = bytearray([0, 0]) - util.set_int(test_bytes_3, 0, test_value_3) + snap7.util.setters.set_int(test_bytes_3, 0, test_value_3) self.client.db_write(db, 8, test_bytes_3) test_values = [test_value_1, test_value_2, test_value_3] @@ -131,15 +132,14 @@ def test_read_multi_vars(self): # create the buffer dataBuffer = ctypes.create_string_buffer(di.Amount) # get a pointer to the buffer - pBuffer = ctypes.cast(ctypes.pointer(dataBuffer), - ctypes.POINTER(ctypes.c_uint8)) + pBuffer = ctypes.cast(ctypes.pointer(dataBuffer), ctypes.POINTER(ctypes.c_uint8)) di.pData = pBuffer result, data_items = self.client.read_multi_vars(data_items) result_values = [] # function to cast bytes to match data_types[] above - byte_to_value = [util.get_real, util.get_real, util.get_int] + byte_to_value = [snap7.util.getters.get_real, snap7.util.getters.get_real, snap7.util.getters.get_int] # unpack and test the result of each read for i in range(len(data_items)): @@ -177,7 +177,7 @@ def test_read_area(self): # Test read_area with a DB area = Areas.DB dbnumber = 1 - data = bytearray(b'\x11') + data = bytearray(b"\x11") self.client.write_area(area, dbnumber, start, data) res = self.client.read_area(area, dbnumber, start, amount) self.assertEqual(data, bytearray(res)) @@ -185,7 +185,7 @@ def test_read_area(self): # Test read_area with a TM area = Areas.TM dbnumber = 0 - data = bytearray(b'\x12\x34') + data = bytearray(b"\x12\x34") self.client.write_area(area, dbnumber, start, data) res = self.client.read_area(area, dbnumber, start, amount) self.assertEqual(data, bytearray(res)) @@ -193,7 +193,7 @@ def test_read_area(self): # Test read_area with a CT area = Areas.CT dbnumber = 0 - data = bytearray(b'\x13\x35') + data = bytearray(b"\x13\x35") self.client.write_area(area, dbnumber, start, data) res = self.client.read_area(area, dbnumber, start, amount) self.assertEqual(data, bytearray(res)) @@ -203,7 +203,7 @@ def test_write_area(self): area = Areas.DB dbnumber = 1 start = 1 - data = bytearray(b'\x11') + data = bytearray(b"\x11") self.client.write_area(area, dbnumber, start, data) res = self.client.read_area(area, dbnumber, start, 1) self.assertEqual(data, bytearray(res)) @@ -211,7 +211,7 @@ def test_write_area(self): # Test write area with a TM area = Areas.TM dbnumber = 0 - timer = bytearray(b'\x12\x00') + timer = bytearray(b"\x12\x00") res = self.client.write_area(area, dbnumber, start, timer) res = self.client.read_area(area, dbnumber, start, 1) self.assertEqual(timer, bytearray(res)) @@ -219,7 +219,7 @@ def test_write_area(self): # Test write area with a CT area = Areas.CT dbnumber = 0 - timer = bytearray(b'\x13\x00') + timer = bytearray(b"\x13\x00") res = self.client.write_area(area, dbnumber, start, timer) res = self.client.read_area(area, dbnumber, start, 1) self.assertEqual(timer, bytearray(res)) @@ -228,24 +228,23 @@ def test_list_blocks(self): self.client.list_blocks() def test_list_blocks_of_type(self): - self.client.list_blocks_of_type('DB', 10) + self.client.list_blocks_of_type("DB", 10) - self.assertRaises(ValueError, self.client.list_blocks_of_type, 'NOblocktype', 10) + self.assertRaises(ValueError, self.client.list_blocks_of_type, "NOblocktype", 10) def test_get_block_info(self): """test Cli_GetAgBlockInfo""" - self.client.get_block_info('DB', 1) + self.client.get_block_info("DB", 1) - self.assertRaises(Exception, self.client.get_block_info, - 'NOblocktype', 10) - self.assertRaises(Exception, self.client.get_block_info, 'DB', 10) + self.assertRaises(Exception, self.client.get_block_info, "NOblocktype", 10) + self.assertRaises(Exception, self.client.get_block_info, "DB", 10) def test_get_cpu_state(self): """this tests the get_cpu_state function""" self.client.get_cpu_state() def test_set_session_password(self): - password = 'abcdefgh' # noqa: S105 + password = "abcdefgh" # noqa: S105 self.client.set_session_password(password) def test_clear_session_password(self): @@ -278,7 +277,7 @@ def test_ab_write(self): self.assertEqual(0, result) def test_as_ab_read(self): - expected = b'\x10\x01' + expected = b"\x10\x01" self.client.ab_write(0, bytearray(expected)) wordlen = WordLen.Byte @@ -290,7 +289,7 @@ def test_as_ab_read(self): self.assertEqual(expected, bytearray(buffer)) def test_as_ab_write(self): - data = b'\x01\x11' + data = b"\x01\x11" response = self.client.as_ab_write(0, bytearray(data)) result = self.client.wait_as_completion(500) self.assertEqual(0, response) @@ -321,8 +320,7 @@ def test_set_param(self): for param, value in values: self.client.set_param(param, value) - self.assertRaises(Exception, self.client.set_param, - snap7.types.RemotePort, 1) + self.assertRaises(Exception, self.client.set_param, snap7.types.RemotePort, 1) def test_get_param(self): expected = ( @@ -338,9 +336,15 @@ def test_get_param(self): for param, value in expected: self.assertEqual(self.client.get_param(param), value) - non_client = (snap7.types.LocalPort, snap7.types.WorkInterval, snap7.types.MaxClients, - snap7.types.BSendTimeout, snap7.types.BRecvTimeout, snap7.types.RecoveryTime, - snap7.types.KeepAliveTime) + non_client = ( + snap7.types.LocalPort, + snap7.types.WorkInterval, + snap7.types.MaxClients, + snap7.types.BSendTimeout, + snap7.types.BRecvTimeout, + snap7.types.RecoveryTime, + snap7.types.KeepAliveTime, + ) # invalid param for client for param in non_client: @@ -353,7 +357,7 @@ def test_as_copy_ram_to_rom(self): def test_as_ct_read(self): # Cli_AsCTRead - expected = b'\x10\x01' + expected = b"\x10\x01" self.client.ct_write(0, 1, bytearray(expected)) type_ = snap7.types.wordlen_to_ctypes[WordLen.Counter.value] buffer = (type_ * 1)() @@ -363,7 +367,7 @@ def test_as_ct_read(self): def test_as_ct_write(self): # Cli_CTWrite - data = b'\x01\x11' + data = b"\x01\x11" response = self.client.as_ct_write(0, 1, bytearray(data)) result = self.client.wait_as_completion(500) self.assertEqual(0, response) @@ -372,7 +376,7 @@ def test_as_ct_write(self): def test_as_db_fill(self): filler = 31 - expected = bytearray(filler.to_bytes(1, byteorder='big') * 100) + expected = bytearray(filler.to_bytes(1, byteorder="big") * 100) self.client.db_fill(1, filler) self.client.wait_as_completion(500) self.assertEqual(expected, self.client.db_read(1, 0, 100)) @@ -382,7 +386,7 @@ def test_as_db_get(self): size = ctypes.c_int(buffer_size) self.client.as_db_get(db_number, _buffer, size) self.client.wait_as_completion(500) - result = bytearray(_buffer)[:size.value] + result = bytearray(_buffer)[: size.value] self.assertEqual(100, len(result)) def test_as_db_read(self): @@ -431,22 +435,22 @@ def test_get_pdu_length(self): def test_get_cpu_info(self): expected = ( - ('ModuleTypeName', 'CPU 315-2 PN/DP'), - ('SerialNumber', 'S C-C2UR28922012'), - ('ASName', 'SNAP7-SERVER'), - ('Copyright', 'Original Siemens Equipment'), - ('ModuleName', 'CPU 315-2 PN/DP') + ("ModuleTypeName", "CPU 315-2 PN/DP"), + ("SerialNumber", "S C-C2UR28922012"), + ("ASName", "SNAP7-SERVER"), + ("Copyright", "Original Siemens Equipment"), + ("ModuleName", "CPU 315-2 PN/DP"), ) cpuInfo = self.client.get_cpu_info() for param, value in expected: - self.assertEqual(getattr(cpuInfo, param).decode('utf-8'), value) + self.assertEqual(getattr(cpuInfo, param).decode("utf-8"), value) def test_db_write_with_byte_literal_does_not_throw(self): mock_write = mock.MagicMock() mock_write.return_value = None original = self.client._library.Cli_DBWrite self.client._library.Cli_DBWrite = mock_write - data = b'\xDE\xAD\xBE\xEF' + data = b"\xde\xad\xbe\xef" try: self.client.db_write(db_number=1, start=0, data=bytearray(data)) @@ -460,7 +464,7 @@ def test_download_with_byte_literal_does_not_throw(self): mock_download.return_value = None original = self.client._library.Cli_Download self.client._library.Cli_Download = mock_download - data = b'\xDE\xAD\xBE\xEF' + data = b"\xde\xad\xbe\xef" try: self.client.download(block_num=db_number, data=bytearray(data)) @@ -478,7 +482,7 @@ def test_write_area_with_byte_literal_does_not_throw(self): area = Areas.DB dbnumber = 1 start = 1 - data = b'\xDE\xAD\xBE\xEF' + data = b"\xde\xad\xbe\xef" try: self.client.write_area(area, dbnumber, start, bytearray(data)) @@ -494,7 +498,7 @@ def test_ab_write_with_byte_literal_does_not_throw(self): self.client._library.Cli_ABWrite = mock_write start = 1 - data = b'\xDE\xAD\xBE\xEF' + data = b"\xde\xad\xbe\xef" try: self.client.ab_write(start=start, data=bytearray(data)) @@ -511,7 +515,7 @@ def test_as_ab_write_with_byte_literal_does_not_throw(self): self.client._library.Cli_AsABWrite = mock_write start = 1 - data = b'\xDE\xAD\xBE\xEF' + data = b"\xde\xad\xbe\xef" try: self.client.as_ab_write(start=start, data=bytearray(data)) @@ -526,7 +530,7 @@ def test_as_db_write_with_byte_literal_does_not_throw(self): mock_write.return_value = None original = self.client._library.Cli_AsDBWrite self.client._library.Cli_AsDBWrite = mock_write - data = b'\xDE\xAD\xBE\xEF' + data = b"\xde\xad\xbe\xef" try: self.client.db_write(db_number=1, start=0, data=bytearray(data)) @@ -541,7 +545,7 @@ def test_as_download_with_byte_literal_does_not_throw(self): mock_download.return_value = None original = self.client._library.Cli_AsDownload self.client._library.Cli_AsDownload = mock_download - data = b'\xDE\xAD\xBE\xEF' + data = b"\xde\xad\xbe\xef" try: self.client.as_download(block_num=db_number, data=bytearray(data)) @@ -594,7 +598,7 @@ def test_wait_as_completion_timeouted(self, timeout=0, tries=500): res = self.client.wait_as_completion(timeout) check_error(res) except RuntimeError as s7_err: - if not s7_err.args[0] == b'CLI : Job Timeout': + if not s7_err.args[0] == b"CLI : Job Timeout": self.fail(f"While waiting another error appeared: {s7_err}") # Wait for a thread to finish time.sleep(0.1) @@ -602,15 +606,17 @@ def test_wait_as_completion_timeouted(self, timeout=0, tries=500): except BaseException: self.fail(f"While waiting another error appeared:>>>>>>>> {res}") - self.fail(f"After {tries} tries, no timout could be envoked by snap7. Either tests are passing to fast or" - f"a problem is existing in the method. Fail test.") + self.fail( + f"After {tries} tries, no timout could be envoked by snap7. Either tests are passing to fast or" + f"a problem is existing in the method. Fail test." + ) def test_check_as_completion(self, timeout=5): # Cli_CheckAsCompletion check_status = ctypes.c_int(-1) pending_checked = False # preparing Server values - data = bytearray(b'\x01\xFF') + data = bytearray(b"\x01\xff") size = len(data) area = Areas.DB db = 1 @@ -631,8 +637,7 @@ def test_check_as_completion(self, timeout=5): else: self.fail(f"TimeoutError - Process pends for more than {timeout} seconds") if pending_checked is False: - logging.warning("Pending was never reached, because Server was to fast," - " but request to server was successfull.") + logging.warning("Pending was never reached, because Server was to fast," " but request to server was successfull.") def test_as_read_area(self): amount = 1 @@ -641,7 +646,7 @@ def test_as_read_area(self): # Test read_area with a DB area = Areas.DB dbnumber = 1 - data = bytearray(b'\x11') + data = bytearray(b"\x11") self.client.write_area(area, dbnumber, start, data) wordlen, usrdata = self.client._prepare_as_read_area(area, amount) pusrdata = ctypes.byref(usrdata) @@ -652,7 +657,7 @@ def test_as_read_area(self): # Test read_area with a TM area = Areas.TM dbnumber = 0 - data = bytearray(b'\x12\x34') + data = bytearray(b"\x12\x34") self.client.write_area(area, dbnumber, start, data) wordlen, usrdata = self.client._prepare_as_read_area(area, amount) pusrdata = ctypes.byref(usrdata) @@ -663,7 +668,7 @@ def test_as_read_area(self): # Test read_area with a CT area = Areas.CT dbnumber = 0 - data = bytearray(b'\x13\x35') + data = bytearray(b"\x13\x35") self.client.write_area(area, dbnumber, start, data) wordlen, usrdata = self.client._prepare_as_read_area(area, amount) pusrdata = ctypes.byref(usrdata) @@ -677,7 +682,7 @@ def test_as_write_area(self): dbnumber = 1 size = 1 start = 1 - data = bytearray(b'\x11') + data = bytearray(b"\x11") wordlen, cdata = self.client._prepare_as_write_area(area, data) res = self.client.as_write_area(area, dbnumber, start, size, wordlen, cdata) self.client.wait_as_completion(1000) @@ -688,7 +693,7 @@ def test_as_write_area(self): area = Areas.TM dbnumber = 0 size = 2 - timer = bytearray(b'\x12\x00') + timer = bytearray(b"\x12\x00") wordlen, cdata = self.client._prepare_as_write_area(area, timer) res = self.client.as_write_area(area, dbnumber, start, size, wordlen, cdata) self.client.wait_as_completion(1000) @@ -699,7 +704,7 @@ def test_as_write_area(self): area = Areas.CT dbnumber = 0 size = 2 - timer = bytearray(b'\x13\x00') + timer = bytearray(b"\x13\x00") wordlen, cdata = self.client._prepare_as_write_area(area, timer) res = self.client.as_write_area(area, dbnumber, start, size, wordlen, cdata) self.client.wait_as_completion(1000) @@ -717,19 +722,19 @@ def test_as_eb_read(self): def test_as_eb_write(self): # Cli_AsEBWrite - response = self.client.as_eb_write(0, 1, bytearray(b'\x00')) + response = self.client.as_eb_write(0, 1, bytearray(b"\x00")) self.assertEqual(0, response) self.assertRaises(RuntimeError, self.client.wait_as_completion, 500) def test_as_full_upload(self): # Cli_AsFullUpload - self.client.as_full_upload('DB', 1) + self.client.as_full_upload("DB", 1) self.assertRaises(RuntimeError, self.client.wait_as_completion, 500) def test_as_list_blocks_of_type(self): data = (ctypes.c_uint16 * 10)() count = ctypes.c_int() - self.client.as_list_blocks_of_type('DB', data, count) + self.client.as_list_blocks_of_type("DB", data, count) self.assertRaises(RuntimeError, self.client.wait_as_completion, 500) def test_as_mb_read(self): @@ -743,14 +748,14 @@ def test_as_mb_read(self): def test_as_mb_write(self): # Cli_AsMBWrite - response = self.client.as_mb_write(0, 1, bytearray(b'\x00')) + response = self.client.as_mb_write(0, 1, bytearray(b"\x00")) self.assertEqual(0, response) self.assertRaises(RuntimeError, self.client.wait_as_completion, 500) def test_as_read_szl(self): # Cli_AsReadSZL - expected = b'S C-C2UR28922012\x00\x00\x00\x00\x00\x00\x00\x00' - ssl_id = 0x011c + expected = b"S C-C2UR28922012\x00\x00\x00\x00\x00\x00\x00\x00" + ssl_id = 0x011C index = 0x0005 s7_szl = S7SZL() size = ctypes.c_int(ctypes.sizeof(s7_szl)) @@ -761,7 +766,7 @@ def test_as_read_szl(self): def test_as_read_szl_list(self): # Cli_AsReadSZLList - expected = b'\x00\x00\x00\x0f\x02\x00\x11\x00\x11\x01\x11\x0f\x12\x00\x12\x01' + expected = b"\x00\x00\x00\x0f\x02\x00\x11\x00\x11\x01\x11\x0f\x12\x00\x12\x01" szl_list = S7SZLList() items_count = ctypes.c_int(ctypes.sizeof(szl_list)) self.client.as_read_szl_list(szl_list, items_count) @@ -771,7 +776,7 @@ def test_as_read_szl_list(self): def test_as_tm_read(self): # Cli_AsMBRead - expected = b'\x10\x01' + expected = b"\x10\x01" wordlen = WordLen.Timer self.client.tm_write(0, 1, bytearray(expected)) type_ = snap7.types.wordlen_to_ctypes[wordlen.value] @@ -782,7 +787,7 @@ def test_as_tm_read(self): def test_as_tm_write(self): # Cli_AsMBWrite - data = b'\x10\x01' + data = b"\x10\x01" response = self.client.as_tm_write(0, 1, bytearray(data)) result = self.client.wait_as_completion(500) self.assertEqual(0, response) @@ -795,21 +800,21 @@ def test_copy_ram_to_rom(self): def test_ct_read(self): # Cli_CTRead - data = b'\x10\x01' + data = b"\x10\x01" self.client.ct_write(0, 1, bytearray(data)) result = self.client.ct_read(0, 1) self.assertEqual(data, result) def test_ct_write(self): # Cli_CTWrite - data = b'\x01\x11' + data = b"\x01\x11" self.assertEqual(0, self.client.ct_write(0, 1, bytearray(data))) self.assertRaises(ValueError, self.client.ct_write, 0, 2, bytes(1)) def test_db_fill(self): # Cli_DBFill filler = 31 - expected = bytearray(filler.to_bytes(1, byteorder='big') * 100) + expected = bytearray(filler.to_bytes(1, byteorder="big") * 100) self.client.db_fill(1, filler) self.assertEqual(expected, self.client.db_read(1, 0, 100)) @@ -823,7 +828,7 @@ def test_eb_read(self): def test_eb_write(self): # Cli_EBWrite self.client._library.Cli_EBWrite = mock.Mock(return_value=0) - response = self.client.eb_write(0, 1, bytearray(b'\x00')) + response = self.client.eb_write(0, 1, bytearray(b"\x00")) self.assertEqual(0, response) def test_error_text(self): @@ -831,9 +836,9 @@ def test_error_text(self): CPU_INVALID_PASSWORD = 0x01E00000 CPU_INVLID_VALUE = 0x00D00000 CANNOT_CHANGE_PARAM = 0x02600000 - self.assertEqual('CPU : Invalid password', self.client.error_text(CPU_INVALID_PASSWORD)) - self.assertEqual('CPU : Invalid value supplied', self.client.error_text(CPU_INVLID_VALUE)) - self.assertEqual('CLI : Cannot change this param now', self.client.error_text(CANNOT_CHANGE_PARAM)) + self.assertEqual("CPU : Invalid password", self.client.error_text(CPU_INVALID_PASSWORD)) + self.assertEqual("CPU : Invalid value supplied", self.client.error_text(CPU_INVLID_VALUE)) + self.assertEqual("CLI : Cannot change this param now", self.client.error_text(CANNOT_CHANGE_PARAM)) def test_get_cp_info(self): # Cli_GetCpInfo @@ -854,7 +859,7 @@ def test_get_last_error(self): def test_get_order_code(self): # Cli_GetOrderCode - expected = b'6ES7 315-2EH14-0AB0 ' + expected = b"6ES7 315-2EH14-0AB0 " result = self.client.get_order_code() self.assertEqual(expected, result.OrderCode) @@ -868,12 +873,14 @@ def test_get_protection(self): self.assertEqual(0, result.anl_sch) def test_get_pg_block_info(self): - valid_db_block = b'pp\x01\x01\x05\n\x00c\x00\x00\x00t\x00\x00\x00\x00\x01\x8d\xbe)2\xa1\x01' \ - b'\x85V\x1f2\xa1\x00*\x00\x00\x00\x00\x00\x02\x01\x0f\x05c\x00#\x00\x00\x00' \ - b'\x11\x04\x10\x01\x04\x01\x04\x01\x04\x01\x04\x01\x04\x01\x04\x01\x04\x01' \ - b'\x04\x01\x04\x01\x04\x01\x04\x01\x04\x01\x04\x01\x04\x01\x04\x01\x04\x00' \ - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ - b'\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + valid_db_block = ( + b"pp\x01\x01\x05\n\x00c\x00\x00\x00t\x00\x00\x00\x00\x01\x8d\xbe)2\xa1\x01" + b"\x85V\x1f2\xa1\x00*\x00\x00\x00\x00\x00\x02\x01\x0f\x05c\x00#\x00\x00\x00" + b"\x11\x04\x10\x01\x04\x01\x04\x01\x04\x01\x04\x01\x04\x01\x04\x01\x04\x01" + b"\x04\x01\x04\x01\x04\x01\x04\x01\x04\x01\x04\x01\x04\x01\x04\x01\x04\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + ) block_info = self.client.get_pg_block_info(bytearray(valid_db_block)) self.assertEqual(10, block_info.BlkType) self.assertEqual(99, block_info.BlkNumber) @@ -883,11 +890,11 @@ def test_get_pg_block_info(self): def test_iso_exchange_buffer(self): # Cli_IsoExchangeBuffer - self.client.db_write(1, 0, bytearray(b'\x11')) + self.client.db_write(1, 0, bytearray(b"\x11")) # PDU read DB1 1.0 BYTE - data = b'\x32\x01\x00\x00\x01\x00\x00\x0e\x00\x00\x04\x01\x12\x0a\x10\x02\x00\x01\x00\x01\x84\x00\x00\x00' + data = b"\x32\x01\x00\x00\x01\x00\x00\x0e\x00\x00\x04\x01\x12\x0a\x10\x02\x00\x01\x00\x01\x84\x00\x00\x00" # PDU response - expected = bytearray(b'2\x03\x00\x00\x01\x00\x00\x02\x00\x05\x00\x00\x04\x01\xff\x04\x00\x08\x11') + expected = bytearray(b"2\x03\x00\x00\x01\x00\x00\x02\x00\x05\x00\x00\x04\x01\xff\x04\x00\x08\x11") self.assertEqual(expected, self.client.iso_exchange_buffer(bytearray(data))) def test_mb_read(self): @@ -900,40 +907,40 @@ def test_mb_read(self): def test_mb_write(self): # Cli_MBWrite self.client._library.Cli_MBWrite = mock.Mock(return_value=0) - response = self.client.mb_write(0, 1, bytearray(b'\x00')) + response = self.client.mb_write(0, 1, bytearray(b"\x00")) self.assertEqual(0, response) def test_read_szl(self): # read_szl_partial_list expected_number_of_records = 10 expected_length_of_record = 34 - ssl_id = 0x001c + ssl_id = 0x001C response = self.client.read_szl(ssl_id) self.assertEqual(expected_number_of_records, response.Header.NDR) self.assertEqual(expected_length_of_record, response.Header.LengthDR) # read_szl_single_data_record - expected = b'S C-C2UR28922012\x00\x00\x00\x00\x00\x00\x00\x00' - ssl_id = 0x011c + expected = b"S C-C2UR28922012\x00\x00\x00\x00\x00\x00\x00\x00" + ssl_id = 0x011C index = 0x0005 response = self.client.read_szl(ssl_id, index) result = bytes(response.Data)[2:26] self.assertEqual(expected, result) # read_szl_order_number - expected = b'6ES7 315-2EH14-0AB0 ' + expected = b"6ES7 315-2EH14-0AB0 " ssl_id = 0x0111 index = 0x0001 response = self.client.read_szl(ssl_id, index) result = bytes(response.Data[2:22]) self.assertEqual(expected, result) # read_szl_invalid_id - ssl_id = 0xffff - index = 0xffff + ssl_id = 0xFFFF + index = 0xFFFF self.assertRaises(RuntimeError, self.client.read_szl, ssl_id) self.assertRaises(RuntimeError, self.client.read_szl, ssl_id, index) def test_read_szl_list(self): # Cli_ReadSZLList - expected = b'\x00\x00\x00\x0f\x02\x00\x11\x00\x11\x01\x11\x0f\x12\x00\x12\x01' + expected = b"\x00\x00\x00\x0f\x02\x00\x11\x00\x11\x01\x11\x0f\x12\x00\x12\x01" result = self.client.read_szl_list() self.assertEqual(expected, result[:16]) @@ -943,14 +950,14 @@ def test_set_plc_system_datetime(self): def test_tm_read(self): # Cli_TMRead - data = b'\x10\x01' + data = b"\x10\x01" self.client.tm_write(0, 1, bytearray(data)) result = self.client.tm_read(0, 1) self.assertEqual(data, result) def test_tm_write(self): # Cli_TMWrite - data = b'\x10\x01' + data = b"\x10\x01" self.assertEqual(0, self.client.tm_write(0, 1, bytearray(data))) self.assertEqual(data, self.client.tm_read(0, 1)) self.assertRaises(RuntimeError, self.client.tm_write, 0, 100, bytes(200)) @@ -962,7 +969,7 @@ def test_write_multi_vars(self): items = [] areas = [Areas.DB, Areas.CT, Areas.TM] expected_list = [] - for i in range(0, items_count): + for i in range(items_count): item = S7DataItem() item.Area = ctypes.c_int32(areas[i].value) wordlen = WordLen.Byte @@ -970,7 +977,7 @@ def test_write_multi_vars(self): item.DBNumber = ctypes.c_int32(1) item.Start = ctypes.c_int32(0) item.Amount = ctypes.c_int32(4) - data = (i + 1).to_bytes(1, byteorder='big') * 4 + data = (i + 1).to_bytes(1, byteorder="big") * 4 array_class = ctypes.c_uint8 * len(data) cdata = array_class.from_buffer_copy(data) item.pData = ctypes.cast(cdata, ctypes.POINTER(array_class)).contents @@ -982,7 +989,7 @@ def test_write_multi_vars(self): self.assertEqual(expected_list[1], self.client.ct_read(0, 2)) self.assertEqual(expected_list[2], self.client.tm_read(0, 2)) - @unittest.skipIf(platform.system() in ['Windows', 'Darwin'], 'Access Violation error') + @unittest.skipIf(platform.system() in ["Windows", "Darwin"], "Access Violation error") def test_set_as_callback(self): expected = b"\x11\x11" self.callback_counter = 0 @@ -1029,7 +1036,7 @@ def test_set_param(self): class TestLibraryIntegration(unittest.TestCase): def setUp(self): # replace the function load_library with a mock - self.loadlib_patch = mock.patch('snap7.client.load_library') + self.loadlib_patch = mock.patch("snap7.client.load_library") self.loadlib_func = self.loadlib_patch.start() # have load_library return another mock @@ -1047,7 +1054,7 @@ def test_create(self): snap7.client.Client() self.mocklib.Cli_Create.assert_called_once() - @mock.patch('snap7.client.byref') + @mock.patch("snap7.client.byref") def test_gc(self, byref_mock): client = snap7.client.Client() client._pointer = 10 @@ -1056,5 +1063,5 @@ def test_gc(self, byref_mock): self.mocklib.Cli_Destroy.assert_called_once() -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_common.py b/tests/test_common.py index f3f2995c..df35ba66 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -13,7 +13,6 @@ @pytest.mark.common class TestCommon(unittest.TestCase): - @classmethod def setUpClass(cls): pass @@ -35,5 +34,5 @@ def test_find_locally(self): self.assertEqual(file, str(self.BASE_DIR / file_name_test)) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_logo_client.py b/tests/test_logo_client.py index 28397dd4..277e6a40 100644 --- a/tests/test_logo_client.py +++ b/tests/test_logo_client.py @@ -9,7 +9,7 @@ logging.basicConfig(level=logging.WARNING) -ip = '127.0.0.1' +ip = "127.0.0.1" tcpport = 1102 db_number = 1 rack = 0x1000 @@ -18,7 +18,6 @@ @pytest.mark.logo class TestLogoClient(unittest.TestCase): - process = None @classmethod @@ -70,8 +69,7 @@ def test_set_param(self): for param, value in values: self.client.set_param(param, value) - self.assertRaises(Exception, self.client.set_param, - snap7.types.RemotePort, 1) + self.assertRaises(Exception, self.client.set_param, snap7.types.RemotePort, 1) def test_get_param(self): expected = ( @@ -87,9 +85,15 @@ def test_get_param(self): for param, value in expected: self.assertEqual(self.client.get_param(param), value) - non_client = (snap7.types.LocalPort, snap7.types.WorkInterval, snap7.types.MaxClients, - snap7.types.BSendTimeout, snap7.types.BRecvTimeout, snap7.types.RecoveryTime, - snap7.types.KeepAliveTime) + non_client = ( + snap7.types.LocalPort, + snap7.types.WorkInterval, + snap7.types.MaxClients, + snap7.types.BSendTimeout, + snap7.types.BRecvTimeout, + snap7.types.RecoveryTime, + snap7.types.KeepAliveTime, + ) # invalid param for client for param in non_client: @@ -120,5 +124,5 @@ def test_set_param(self): self.client.set_param(param, value) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_mainloop.py b/tests/test_mainloop.py index 1dc608a7..34f7fae6 100644 --- a/tests/test_mainloop.py +++ b/tests/test_mainloop.py @@ -3,17 +3,19 @@ import time import pytest import unittest +from typing import Optional import snap7.error import snap7.server import snap7.util +import snap7.util.getters from snap7.util import get_bool, get_dint, get_dword, get_int, get_real, get_sint, get_string, get_usint, get_word from snap7.client import Client import snap7.types logging.basicConfig(level=logging.WARNING) -ip = '127.0.0.1' +ip = "127.0.0.1" tcpport = 1102 db_number = 1 rack = 1 @@ -22,8 +24,8 @@ @pytest.mark.mainloop class TestServer(unittest.TestCase): - - process = None + process: Optional[Process] = None + client: Client @classmethod def setUpClass(cls): @@ -32,31 +34,33 @@ def setUpClass(cls): time.sleep(2) # wait for server to start @classmethod - def tearDownClass(cls): - cls.process.terminate() - cls.process.join(1) - if cls.process.is_alive(): - cls.process.kill() - - def setUp(self): + def tearDownClass(cls) -> None: + if cls.process: + cls.process.terminate() + cls.process.join(1) + if cls.process.is_alive(): + cls.process.kill() + + def setUp(self) -> None: self.client: Client = snap7.client.Client() self.client.connect(ip, rack, slot, tcpport) - def tearDown(self): - self.client.disconnect() - self.client.destroy() + def tearDown(self) -> None: + if self.client: + self.client.disconnect() + self.client.destroy() @unittest.skip("TODO: only first test used") - def test_read_prefill_db(self): + def test_read_prefill_db(self) -> None: data = self.client.db_read(0, 0, 7) - boolean = snap7.util.get_bool(data, 0, 0) + boolean = snap7.util.getters.get_bool(data, 0, 0) self.assertEqual(boolean, True) - integer = snap7.util.get_int(data, 1) + integer = snap7.util.getters.get_int(data, 1) self.assertEqual(integer, 128) - real = snap7.util.get_real(data, 3) + real = snap7.util.getters.get_real(data, 3) self.assertEqual(real, -128) - def test_read_booleans(self): + def test_read_booleans(self) -> None: data = self.client.db_read(0, 0, 1) self.assertEqual(False, get_bool(data, 0, 0)) self.assertEqual(True, get_bool(data, 0, 1)) @@ -67,7 +71,7 @@ def test_read_booleans(self): self.assertEqual(False, get_bool(data, 0, 6)) self.assertEqual(True, get_bool(data, 0, 7)) - def test_read_small_int(self): + def test_read_small_int(self) -> None: data = self.client.db_read(0, 10, 4) value_1 = get_sint(data, 0) value_2 = get_sint(data, 1) @@ -130,7 +134,7 @@ def test_read_double_word(self): self.assertEqual(get_dword(data, 24), 0xFFFFFFFF) -if __name__ == '__main__': +if __name__ == "__main__": import logging logging.basicConfig() diff --git a/tests/test_partner.py b/tests/test_partner.py index 4fc7638b..fa89cb27 100644 --- a/tests/test_partner.py +++ b/tests/test_partner.py @@ -65,8 +65,7 @@ def test_get_param(self): for param, value in expected: self.assertEqual(self.partner.get_param(param), value) - self.assertRaises(Exception, self.partner.get_param, - snap7.types.MaxClients) + self.assertRaises(Exception, self.partner.get_param, snap7.types.MaxClients) def test_get_stats(self): self.partner.get_stats() @@ -95,8 +94,7 @@ def test_set_param(self): for param, value in values: self.partner.set_param(param, value) - self.assertRaises(Exception, self.partner.set_param, - snap7.types.RemotePort, 1) + self.assertRaises(Exception, self.partner.set_param, snap7.types.RemotePort, 1) def test_set_recv_callback(self): self.partner.set_recv_callback() @@ -108,7 +106,7 @@ def test_start(self): self.partner.start() def test_start_to(self): - self.partner.start_to('0.0.0.0', '0.0.0.0', 0, 0) # noqa: S104 + self.partner.start_to("0.0.0.0", "0.0.0.0", 0, 0) # noqa: S104 def test_stop(self): self.partner.stop() @@ -121,7 +119,7 @@ def test_wait_as_b_send_completion(self): class TestLibraryIntegration(unittest.TestCase): def setUp(self): # replace the function load_library with a mock - self.loadlib_patch = mock.patch('snap7.partner.load_library') + self.loadlib_patch = mock.patch("snap7.partner.load_library") self.loadlib_func = self.loadlib_patch.start() # have load_library return another mock @@ -145,5 +143,5 @@ def test_gc(self): self.mocklib.Par_Destroy.assert_called_once() -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_server.py b/tests/test_server.py index b22326e9..561d6683 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -13,7 +13,6 @@ @pytest.mark.server class TestServer(unittest.TestCase): - def setUp(self): self.server = snap7.server.Server() self.server.start(tcpport=1102) @@ -45,6 +44,7 @@ def test_get_mask(self): def test_lock_area(self): from threading import Thread + area_code = snap7.types.srvAreaDB index = 1 db1_type = ctypes.c_char * 1024 @@ -116,8 +116,8 @@ def test_clear_events(self): self.assertFalse(self.server.clear_events()) def test_start_to(self): - self.server.start_to('0.0.0.0') # noqa: S104 - self.assertRaises(ValueError, self.server.start_to, 'bogus') + self.server.start_to("0.0.0.0") # noqa: S104 + self.assertRaises(ValueError, self.server.start_to, "bogus") def test_get_param(self): # check the defaults @@ -126,8 +126,7 @@ def test_get_param(self): self.assertEqual(self.server.get_param(snap7.types.MaxClients), 1024) # invalid param for server - self.assertRaises(Exception, self.server.get_param, - snap7.types.RemotePort) + self.assertRaises(Exception, self.server.get_param, snap7.types.RemotePort) @pytest.mark.server @@ -147,7 +146,7 @@ def test_set_param(self): class TestLibraryIntegration(unittest.TestCase): def setUp(self): # replace the function load_library with a mock - self.loadlib_patch = mock.patch('snap7.server.load_library') + self.loadlib_patch = mock.patch("snap7.server.load_library") self.loadlib_func = self.loadlib_patch.start() # have load_library return another mock @@ -171,7 +170,7 @@ def test_gc(self): self.mocklib.Srv_Destroy.assert_called_once() -if __name__ == '__main__': +if __name__ == "__main__": import logging logging.basicConfig() diff --git a/tests/test_util.py b/tests/test_util.py index 90e03f49..81d41de6 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -1,10 +1,12 @@ import datetime -import re import pytest import unittest import struct -from snap7 import util, types +import snap7.util.db +import snap7.util.getters +import snap7.util.setters +from snap7 import types test_spec = """ @@ -78,76 +80,154 @@ """ -_bytearray = bytearray([ - 0, 0, # test int - 4, 4, ord('t'), ord('e'), ord('s'), ord('t'), # test string - 128 * 0 + 64 * 0 + 32 * 0 + 16 * 0 - + 8 * 1 + 4 * 1 + 2 * 1 + 1 * 1, # test bools - 68, 78, 211, 51, # test real - 255, 255, 255, 255, # test dword - 0, 0, # test int 2 - 128, 0, 0, 0, # test dint - 255, 255, # test word - 0, 16, # test s5time, 0 is the time base, - # 16 is value, those two integers should be declared together - 32, 7, 18, 23, 50, 2, 133, 65, # these 8 values build the date and time 12 byte total - # data typ together, for details under this link - # https://support.industry.siemens.com/cs/document/36479/date_and_time-format-bei-s7-?dti=0&lc=de-DE - 254, 254, 254, 254, 254, 127, # test small int - 128, # test set byte - 143, 255, 255, 255, # test time - 254, # test byte 0xFE - 48, 57, # test uint 12345 - 7, 91, 205, 21, # test udint 123456789 - 65, 157, 111, 52, 84, 126, 107, 117, # test lreal 123456789.123456789 - 65, # test char A - 3, 169, # test wchar Ω - 0, 4, 0, 4, 3, 169, 0, ord('s'), 0, ord('t'), 0, 196, # test wstring Ω s t Ä - 45, 235, # test date 09.03.2022 - 2, 179, 41, 128, # test tod 12:34:56 - 7, 230, 3, 9, 4, 12, 34, 45, 0, 0, 0, 0, # test dtl 09.03.2022 12:34:56 - 116, 101, 115, 116, 32, 32, 32, 32 # test fstring 'test ' -]) +_bytearray = bytearray( + [ + 0, + 0, # test int + 4, + 4, + ord("t"), + ord("e"), + ord("s"), + ord("t"), # test string + 128 * 0 + 64 * 0 + 32 * 0 + 16 * 0 + 8 * 1 + 4 * 1 + 2 * 1 + 1 * 1, # test bools + 68, + 78, + 211, + 51, # test real + 255, + 255, + 255, + 255, # test dword + 0, + 0, # test int 2 + 128, + 0, + 0, + 0, # test dint + 255, + 255, # test word + 0, + 16, # test s5time, 0 is the time base, + # 16 is value, those two integers should be declared together + 32, + 7, + 18, + 23, + 50, + 2, + 133, + 65, # these 8 values build the date and time 12 byte total + # data typ together, for details under this link + # https://support.industry.siemens.com/cs/document/36479/date_and_time-format-bei-s7-?dti=0&lc=de-DE + 254, + 254, + 254, + 254, + 254, + 127, # test small int + 128, # test set byte + 143, + 255, + 255, + 255, # test time + 254, # test byte 0xFE + 48, + 57, # test uint 12345 + 7, + 91, + 205, + 21, # test udint 123456789 + 65, + 157, + 111, + 52, + 84, + 126, + 107, + 117, # test lreal 123456789.123456789 + 65, # test char A + 3, + 169, # test wchar Ω + 0, + 4, + 0, + 4, + 3, + 169, + 0, + ord("s"), + 0, + ord("t"), + 0, + 196, # test wstring Ω s t Ä + 45, + 235, # test date 09.03.2022 + 2, + 179, + 41, + 128, # test tod 12:34:56 + 7, + 230, + 3, + 9, + 4, + 12, + 34, + 45, + 0, + 0, + 0, + 0, # test dtl 09.03.2022 12:34:56 + 116, + 101, + 115, + 116, + 32, + 32, + 32, + 32, # test fstring 'test ' + ] +) _new_bytearray = bytearray(100) -_new_bytearray[41:41 + 1] = struct.pack("B", 128) # byte_index=41, value=128, bytes=1 -_new_bytearray[42:42 + 1] = struct.pack("B", 255) # byte_index=41, value=255, bytes=1 -_new_bytearray[43:43 + 4] = struct.pack("I", 286331153) # byte_index=43, value=286331153(T#3D_7H_32M_11S_153MS), bytes=4 +_new_bytearray[41 : 41 + 1] = struct.pack("B", 128) # byte_index=41, value=128, bytes=1 +_new_bytearray[42 : 42 + 1] = struct.pack("B", 255) # byte_index=41, value=255, bytes=1 +_new_bytearray[43 : 43 + 4] = struct.pack("I", 286331153) # byte_index=43, value=286331153(T#3D_7H_32M_11S_153MS), bytes=4 @pytest.mark.util class TestS7util(unittest.TestCase): - def test_get_byte_new(self): test_array = bytearray(_new_bytearray) - byte_ = util.get_byte(test_array, 41) + byte_ = snap7.util.getters.get_byte(test_array, 41) self.assertEqual(byte_, 128) - byte_ = util.get_byte(test_array, 42) + byte_ = snap7.util.getters.get_byte(test_array, 42) self.assertEqual(byte_, 255) def test_set_byte_new(self): test_array = bytearray(_new_bytearray) - util.set_byte(test_array, 41, 127) - byte_ = util.get_byte(test_array, 41) + snap7.util.setters.set_byte(test_array, 41, 127) + byte_ = snap7.util.getters.get_byte(test_array, 41) self.assertEqual(byte_, 127) def test_get_byte(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec, layout_offset=4) - value = row.get_value(50, 'BYTE') # get value + row = snap7.util.db.DB_Row(test_array, test_spec, layout_offset=4) + value = row.get_value(50, "BYTE") # get value self.assertEqual(value, 254) def test_set_byte(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec, layout_offset=4) - row['testByte'] = 255 - self.assertEqual(row['testByte'], 255) + row = snap7.util.db.DB_Row(test_array, test_spec, layout_offset=4) + row["testByte"] = 255 + self.assertEqual(row["testByte"], 255) def test_set_lreal(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec, layout_offset=4) - row['testLreal'] = 123.123 - self.assertEqual(row['testLreal'], 123.123) + row = snap7.util.db.DB_Row(test_array, test_spec, layout_offset=4) + row["testLreal"] = 123.123 + self.assertEqual(row["testLreal"], 123.123) def test_get_s5time(self): """ @@ -155,9 +235,9 @@ def test_get_s5time(self): """ test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec, layout_offset=4) + row = snap7.util.db.DB_Row(test_array, test_spec, layout_offset=4) - self.assertEqual(row['testS5time'], '0:00:00.100000') + self.assertEqual(row["testS5time"], "0:00:00.100000") def test_get_dt(self): """ @@ -165,56 +245,56 @@ def test_get_dt(self): """ test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec, layout_offset=4) + row = snap7.util.db.DB_Row(test_array, test_spec, layout_offset=4) - self.assertEqual(row['testdateandtime'], '2020-07-12T17:32:02.854000') + self.assertEqual(row["testdateandtime"], "2020-07-12T17:32:02.854000") def test_get_time(self): test_values = [ - (0, '0:0:0:0.0'), - (1, '0:0:0:0.1'), # T#1MS - (1000, '0:0:0:1.0'), # T#1S - (60000, '0:0:1:0.0'), # T#1M - (3600000, '0:1:0:0.0'), # T#1H - (86400000, '1:0:0:0.0'), # T#1D - (2147483647, '24:20:31:23.647'), # max range - (-0, '0:0:0:0.0'), - (-1, '-0:0:0:0.1'), # T#-1MS - (-1000, '-0:0:0:1.0'), # T#-1S - (-60000, '-0:0:1:0.0'), # T#-1M - (-3600000, '-0:1:0:0.0'), # T#-1H - (-86400000, '-1:0:0:0.0'), # T#-1D - (-2147483647, '-24:20:31:23.647'), # min range + (0, "0:0:0:0.0"), + (1, "0:0:0:0.1"), # T#1MS + (1000, "0:0:0:1.0"), # T#1S + (60000, "0:0:1:0.0"), # T#1M + (3600000, "0:1:0:0.0"), # T#1H + (86400000, "1:0:0:0.0"), # T#1D + (2147483647, "24:20:31:23.647"), # max range + (-0, "0:0:0:0.0"), + (-1, "-0:0:0:0.1"), # T#-1MS + (-1000, "-0:0:0:1.0"), # T#-1S + (-60000, "-0:0:1:0.0"), # T#-1M + (-3600000, "-0:1:0:0.0"), # T#-1H + (-86400000, "-1:0:0:0.0"), # T#-1D + (-2147483647, "-24:20:31:23.647"), # min range ] data = bytearray(4) for value_to_test, expected_value in test_values: data[:] = struct.pack(">i", value_to_test) - self.assertEqual(util.get_time(data, 0), expected_value) + self.assertEqual(snap7.util.getters.get_time(data, 0), expected_value) def test_set_time(self): test_array = bytearray(_new_bytearray) with self.assertRaises(ValueError): - util.set_time(test_array, 43, '-24:25:30:23:193') + snap7.util.setters.set_time(test_array, 43, "-24:25:30:23:193") with self.assertRaises(ValueError): - util.set_time(test_array, 43, '-24:24:32:11.648') + snap7.util.setters.set_time(test_array, 43, "-24:24:32:11.648") with self.assertRaises(ValueError): - util.set_time(test_array, 43, '-25:23:32:11.648') + snap7.util.setters.set_time(test_array, 43, "-25:23:32:11.648") with self.assertRaises(ValueError): - util.set_time(test_array, 43, '24:24:30:23.620') + snap7.util.setters.set_time(test_array, 43, "24:24:30:23.620") - util.set_time(test_array, 43, '24:20:31:23.647') - byte_ = util.get_time(test_array, 43) - self.assertEqual(byte_, '24:20:31:23.647') + snap7.util.setters.set_time(test_array, 43, "24:20:31:23.647") + byte_ = snap7.util.getters.get_time(test_array, 43) + self.assertEqual(byte_, "24:20:31:23.647") - util.set_time(test_array, 43, '-24:20:31:23.648') - byte_ = util.get_time(test_array, 43) - self.assertEqual(byte_, '-24:20:31:23.648') + snap7.util.setters.set_time(test_array, 43, "-24:20:31:23.648") + byte_ = snap7.util.getters.get_time(test_array, 43) + self.assertEqual(byte_, "-24:20:31:23.648") - util.set_time(test_array, 43, '3:7:32:11.153') - byte_ = util.get_time(test_array, 43) - self.assertEqual(byte_, '3:7:32:11.153') + snap7.util.setters.set_time(test_array, 43, "3:7:32:11.153") + byte_ = snap7.util.getters.get_time(test_array, 43) + self.assertEqual(byte_, "3:7:32:11.153") def test_get_string(self): """ @@ -222,260 +302,240 @@ def test_get_string(self): """ test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec, layout_offset=4) - self.assertEqual(row['NAME'], 'test') + row = snap7.util.db.DB_Row(test_array, test_spec, layout_offset=4) + self.assertEqual(row["NAME"], "test") def test_write_string(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec, layout_offset=4) - row['NAME'] = 'abc' - self.assertEqual(row['NAME'], 'abc') - row['NAME'] = '' - self.assertEqual(row['NAME'], '') + row = snap7.util.db.DB_Row(test_array, test_spec, layout_offset=4) + row["NAME"] = "abc" + self.assertEqual(row["NAME"], "abc") + row["NAME"] = "" + self.assertEqual(row["NAME"], "") try: - row['NAME'] = 'waaaaytoobig' + row["NAME"] = "waaaaytoobig" except ValueError: pass # value should still be empty - self.assertEqual(row['NAME'], '') + self.assertEqual(row["NAME"], "") try: - row['NAME'] = 'TrÖt' + row["NAME"] = "TrÖt" except ValueError: pass def test_get_fstring(self): data = [ord(letter) for letter in "hello world "] - self.assertEqual(util.get_fstring(data, 0, 15), 'hello world') - self.assertEqual(util.get_fstring(data, 0, 15, remove_padding=False), 'hello world ') + self.assertEqual(snap7.util.getters.get_fstring(data, 0, 15), "hello world") + self.assertEqual(snap7.util.getters.get_fstring(data, 0, 15, remove_padding=False), "hello world ") def test_get_fstring_name(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec, layout_offset=4) - value = row['testFstring'] - self.assertEqual(value, 'test') + row = snap7.util.db.DB_Row(test_array, test_spec, layout_offset=4) + value = row["testFstring"] + self.assertEqual(value, "test") def test_get_fstring_index(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec, layout_offset=4) - value = row.get_value(98, 'FSTRING[8]') # get value - self.assertEqual(value, 'test') + row = snap7.util.db.DB_Row(test_array, test_spec, layout_offset=4) + value = row.get_value(98, "FSTRING[8]") # get value + self.assertEqual(value, "test") def test_set_fstring(self): data = bytearray(20) - util.set_fstring(data, 0, "hello world", 15) - self.assertEqual(data, bytearray(b'hello world \x00\x00\x00\x00\x00')) + snap7.util.setters.set_fstring(data, 0, "hello world", 15) + self.assertEqual(data, bytearray(b"hello world \x00\x00\x00\x00\x00")) def test_set_fstring_name(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec, layout_offset=4) - row['testFstring'] = 'TSET' - self.assertEqual(row['testFstring'], 'TSET') + row = snap7.util.db.DB_Row(test_array, test_spec, layout_offset=4) + row["testFstring"] = "TSET" + self.assertEqual(row["testFstring"], "TSET") def test_set_fstring_index(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec, layout_offset=4) - row.set_value(98, 'FSTRING[8]', 'TSET') - self.assertEqual(row['testFstring'], 'TSET') + row = snap7.util.db.DB_Row(test_array, test_spec, layout_offset=4) + row.set_value(98, "FSTRING[8]", "TSET") + self.assertEqual(row["testFstring"], "TSET") def test_get_int(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec, layout_offset=4) - x = row['ID'] - y = row['testint2'] + row = snap7.util.db.DB_Row(test_array, test_spec, layout_offset=4) + x = row["ID"] + y = row["testint2"] self.assertEqual(x, 0) self.assertEqual(y, 0) def test_set_int(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec, layout_offset=4) - row['ID'] = 259 - self.assertEqual(row['ID'], 259) + row = snap7.util.db.DB_Row(test_array, test_spec, layout_offset=4) + row["ID"] = 259 + self.assertEqual(row["ID"], 259) def test_get_usint(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec, layout_offset=4) - value = row.get_value(43, 'USINT') # get value + row = snap7.util.db.DB_Row(test_array, test_spec, layout_offset=4) + value = row.get_value(43, "USINT") # get value self.assertEqual(value, 254) def test_set_usint(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec, layout_offset=4) - row['testusint0'] = 255 - self.assertEqual(row['testusint0'], 255) + row = snap7.util.db.DB_Row(test_array, test_spec, layout_offset=4) + row["testusint0"] = 255 + self.assertEqual(row["testusint0"], 255) def test_get_sint(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec, layout_offset=4) - value = row.get_value(44, 'SINT') # get value + row = snap7.util.db.DB_Row(test_array, test_spec, layout_offset=4) + value = row.get_value(44, "SINT") # get value self.assertEqual(value, 127) def test_set_sint(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec, layout_offset=4) - row['testsint0'] = 127 - self.assertEqual(row['testsint0'], 127) + row = snap7.util.db.DB_Row(test_array, test_spec, layout_offset=4) + row["testsint0"] = 127 + self.assertEqual(row["testsint0"], 127) def test_set_int_roundtrip(self): DB1 = (types.wordlen_to_ctypes[types.S7WLByte] * 4)() - for i in range(-(2 ** 15) + 1, (2 ** 15) - 1): - util.set_int(DB1, 0, i) - result = util.get_int(DB1, 0) + for i in range(-(2**15) + 1, (2**15) - 1): + snap7.util.setters.set_int(DB1, 0, i) + result = snap7.util.getters.get_int(DB1, 0) self.assertEqual(i, result) def test_get_int_values(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec, layout_offset=4) - for value in ( - -32768, - -16385, - -256, - -128, - -127, - 0, - 127, - 128, - 255, - 256, - 16384, - 32767): - row['ID'] = value - self.assertEqual(row['ID'], value) + row = snap7.util.db.DB_Row(test_array, test_spec, layout_offset=4) + for value in (-32768, -16385, -256, -128, -127, 0, 127, 128, 255, 256, 16384, 32767): + row["ID"] = value + self.assertEqual(row["ID"], value) def test_get_bool(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec, layout_offset=4) - self.assertEqual(row['testbool1'], 1) - self.assertEqual(row['testbool8'], 0) + row = snap7.util.db.DB_Row(test_array, test_spec, layout_offset=4) + self.assertEqual(row["testbool1"], 1) + self.assertEqual(row["testbool8"], 0) def test_set_bool(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec, layout_offset=4) - row['testbool8'] = True - row['testbool1'] = False + row = snap7.util.db.DB_Row(test_array, test_spec, layout_offset=4) + row["testbool8"] = True + row["testbool1"] = False - self.assertEqual(row['testbool8'], True) - self.assertEqual(row['testbool1'], False) + self.assertEqual(row["testbool8"], True) + self.assertEqual(row["testbool1"], False) def test_db_creation(self): test_array = bytearray(_bytearray * 10) - test_db = util.DB(1, test_array, test_spec, - row_size=len(_bytearray), - size=10, - layout_offset=4, - db_offset=0) + test_db = snap7.util.db.DB(1, test_array, test_spec, row_size=len(_bytearray), size=10, layout_offset=4, db_offset=0) self.assertEqual(len(test_db.index), 10) for i, row in test_db: # print row - self.assertEqual(row['testbool1'], 1) - self.assertEqual(row['testbool2'], 1) - self.assertEqual(row['testbool3'], 1) - self.assertEqual(row['testbool4'], 1) + self.assertEqual(row["testbool1"], 1) + self.assertEqual(row["testbool2"], 1) + self.assertEqual(row["testbool3"], 1) + self.assertEqual(row["testbool4"], 1) - self.assertEqual(row['testbool5'], 0) - self.assertEqual(row['testbool6'], 0) - self.assertEqual(row['testbool7'], 0) - self.assertEqual(row['testbool8'], 0) - self.assertEqual(row['NAME'], 'test') + self.assertEqual(row["testbool5"], 0) + self.assertEqual(row["testbool6"], 0) + self.assertEqual(row["testbool7"], 0) + self.assertEqual(row["testbool8"], 0) + self.assertEqual(row["NAME"], "test") def test_db_export(self): test_array = bytearray(_bytearray * 10) - test_db = util.DB(1, test_array, test_spec, - row_size=len(_bytearray), - size=10, - layout_offset=4, - db_offset=0) + test_db = snap7.util.db.DB(1, test_array, test_spec, row_size=len(_bytearray), size=10, layout_offset=4, db_offset=0) db_export = test_db.export() for i in db_export: - self.assertEqual(db_export[i]['testbool1'], 1) - self.assertEqual(db_export[i]['testbool2'], 1) - self.assertEqual(db_export[i]['testbool3'], 1) - self.assertEqual(db_export[i]['testbool4'], 1) + self.assertEqual(db_export[i]["testbool1"], 1) + self.assertEqual(db_export[i]["testbool2"], 1) + self.assertEqual(db_export[i]["testbool3"], 1) + self.assertEqual(db_export[i]["testbool4"], 1) - self.assertEqual(db_export[i]['testbool5'], 0) - self.assertEqual(db_export[i]['testbool6'], 0) - self.assertEqual(db_export[i]['testbool7'], 0) - self.assertEqual(db_export[i]['testbool8'], 0) - self.assertEqual(db_export[i]['NAME'], 'test') + self.assertEqual(db_export[i]["testbool5"], 0) + self.assertEqual(db_export[i]["testbool6"], 0) + self.assertEqual(db_export[i]["testbool7"], 0) + self.assertEqual(db_export[i]["testbool8"], 0) + self.assertEqual(db_export[i]["NAME"], "test") def test_get_real(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec, layout_offset=4) - self.assertTrue(0.01 > (row['testReal'] - 827.3) > -0.1) + row = snap7.util.db.DB_Row(test_array, test_spec, layout_offset=4) + self.assertTrue(0.01 > (row["testReal"] - 827.3) > -0.1) def test_set_real(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec, layout_offset=4) - row['testReal'] = 1337.1337 - self.assertTrue(0.01 > (row['testReal'] - 1337.1337) > -0.01) + row = snap7.util.db.DB_Row(test_array, test_spec, layout_offset=4) + row["testReal"] = 1337.1337 + self.assertTrue(0.01 > (row["testReal"] - 1337.1337) > -0.01) def test_set_dword(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec, layout_offset=4) + row = snap7.util.db.DB_Row(test_array, test_spec, layout_offset=4) # The range of numbers is 0 to 4294967295. - row['testDword'] = 9999999 - self.assertEqual(row['testDword'], 9999999) + row["testDword"] = 9999999 + self.assertEqual(row["testDword"], 9999999) def test_get_dword(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec, layout_offset=4) - self.assertEqual(row['testDword'], 4294967295) + row = snap7.util.db.DB_Row(test_array, test_spec, layout_offset=4) + self.assertEqual(row["testDword"], 4294967295) def test_set_dint(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec, layout_offset=4) + row = snap7.util.db.DB_Row(test_array, test_spec, layout_offset=4) # The range of numbers is -2147483648 to 2147483647 + - row.set_value(23, 'DINT', 2147483647) # set value - self.assertEqual(row['testDint'], 2147483647) + row.set_value(23, "DINT", 2147483647) # set value + self.assertEqual(row["testDint"], 2147483647) def test_get_dint(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec, layout_offset=4) - value = row.get_value(23, 'DINT') # get value + row = snap7.util.db.DB_Row(test_array, test_spec, layout_offset=4) + value = row.get_value(23, "DINT") # get value self.assertEqual(value, -2147483648) def test_set_word(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec, layout_offset=4) + row = snap7.util.db.DB_Row(test_array, test_spec, layout_offset=4) # The range of numbers is 0 to 65535 - row.set_value(27, 'WORD', 0) # set value - self.assertEqual(row['testWord'], 0) + row.set_value(27, "WORD", 0) # set value + self.assertEqual(row["testWord"], 0) def test_get_word(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec, layout_offset=4) - value = row.get_value(27, 'WORD') # get value + row = snap7.util.db.DB_Row(test_array, test_spec, layout_offset=4) + value = row.get_value(27, "WORD") # get value self.assertEqual(value, 65535) def test_export(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec, layout_offset=4) + row = snap7.util.db.DB_Row(test_array, test_spec, layout_offset=4) data = row.export() - self.assertIn('testDword', data) - self.assertIn('testbool1', data) - self.assertEqual(data['testbool5'], 0) + self.assertIn("testDword", data) + self.assertIn("testbool1", data) + self.assertEqual(data["testbool5"], 0) def test_indented_layout(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec_indented, layout_offset=4) - x = row['ID'] - y_single_space = row['testbool1'] - y_multi_space = row['testbool2'] - y_single_indent = row['testint2'] - y_multi_indent = row['testbool8'] + row = snap7.util.db.DB_Row(test_array, test_spec_indented, layout_offset=4) + x = row["ID"] + y_single_space = row["testbool1"] + y_multi_space = row["testbool2"] + y_single_indent = row["testint2"] + y_multi_indent = row["testbool8"] with self.assertRaises(KeyError): - fail_single_space = row['testbool4'] # noqa: F841 + fail_single_space = row["testbool4"] # noqa: F841 with self.assertRaises(KeyError): - fail_multiple_spaces = row['testbool5'] # noqa: F841 + fail_multiple_spaces = row["testbool5"] # noqa: F841 with self.assertRaises(KeyError): - fail_single_indent = row['testbool6'] # noqa: F841 + fail_single_indent = row["testbool6"] # noqa: F841 with self.assertRaises(KeyError): - fail_multiple_indent = row['testbool7'] # noqa: F841 + fail_multiple_indent = row["testbool7"] # noqa: F841 self.assertEqual(x, 0) self.assertEqual(y_single_space, True) @@ -485,91 +545,58 @@ def test_indented_layout(self): def test_get_uint(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec_indented, layout_offset=4) - val = row['testUint'] + row = snap7.util.db.DB_Row(test_array, test_spec_indented, layout_offset=4) + val = row["testUint"] self.assertEqual(val, 12345) def test_get_udint(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec_indented, layout_offset=4) - val = row['testUdint'] + row = snap7.util.db.DB_Row(test_array, test_spec_indented, layout_offset=4) + val = row["testUdint"] self.assertEqual(val, 123456789) def test_get_lreal(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec_indented, layout_offset=4) - val = row['testLreal'] + row = snap7.util.db.DB_Row(test_array, test_spec_indented, layout_offset=4) + val = row["testLreal"] self.assertEqual(val, 123456789.123456789) def test_get_char(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec_indented, layout_offset=4) - val = row['testChar'] - self.assertEqual(val, 'A') + row = snap7.util.db.DB_Row(test_array, test_spec_indented, layout_offset=4) + val = row["testChar"] + self.assertEqual(val, "A") def test_get_wchar(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec_indented, layout_offset=4) - val = row['testWchar'] - self.assertEqual(val, 'Ω') + row = snap7.util.db.DB_Row(test_array, test_spec_indented, layout_offset=4) + val = row["testWchar"] + self.assertEqual(val, "Ω") def test_get_wstring(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec_indented, layout_offset=4) - val = row['testWstring'] - self.assertEqual(val, 'ΩstÄ') + row = snap7.util.db.DB_Row(test_array, test_spec_indented, layout_offset=4) + val = row["testWstring"] + self.assertEqual(val, "ΩstÄ") def test_get_date(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec_indented, layout_offset=4) - val = row['testDate'] + row = snap7.util.db.DB_Row(test_array, test_spec_indented, layout_offset=4) + val = row["testDate"] self.assertEqual(val, datetime.date(day=9, month=3, year=2022)) def test_get_tod(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec_indented, layout_offset=4) - val = row['testTod'] + row = snap7.util.db.DB_Row(test_array, test_spec_indented, layout_offset=4) + val = row["testTod"] self.assertEqual(val, datetime.timedelta(hours=12, minutes=34, seconds=56)) def test_get_dtl(self): test_array = bytearray(_bytearray) - row = util.DB_Row(test_array, test_spec_indented, layout_offset=4) - val = row['testDtl'] + row = snap7.util.db.DB_Row(test_array, test_spec_indented, layout_offset=4) + val = row["testDtl"] self.assertEqual(val, datetime.datetime(year=2022, month=3, day=9, hour=12, minute=34, second=45)) -def print_row(data): - """print a single db row in chr and str - """ - index_line = "" - pri_line1 = "" - chr_line2 = "" - asci = re.compile('[a-zA-Z0-9 ]') - - for i, xi in enumerate(data): - # index - if not i % 5: - diff = len(pri_line1) - len(index_line) - i = str(i) - index_line += diff * ' ' - index_line += i - # i = i + (ws - len(i)) * ' ' + ',' - - # byte array line - str_v = str(xi) - pri_line1 += str(xi) + ',' - # char line - c = chr(xi) - c = c if asci.match(c) else ' ' - # align white space - w = len(str_v) - c = c + (w - 1) * ' ' + ',' - chr_line2 += c - - print(index_line) - print(pri_line1) - print(chr_line2) - - -if __name__ == '__main__': +if __name__ == "__main__": unittest.main()