├── .github └── workflows │ ├── manual_triggerd_build_and_upload_to_test_pypi.yml │ └── release_triggerd_build_and_upload_to_pypi.yml ├── .gitignore ├── .gitmodules ├── .readthedocs.yaml ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── developer_notes.rst ├── make.bat ├── requirements.txt └── source │ ├── basics.rst │ ├── cdef_slave.rst │ ├── coe_objects.rst │ ├── conf.py │ ├── exceptions.rst │ ├── helpers.rst │ ├── index.rst │ ├── installation.rst │ ├── master.rst │ ├── process_data.rst │ └── requirements.rst ├── examples ├── basic_example.py ├── find_adapters.py ├── firmware_update.py ├── minimal_example.py ├── read_eeprom.py ├── read_sdo_info.py └── write_foe.py ├── pyproject.toml ├── setup.py ├── src ├── pysoem │ ├── __init__.py │ ├── cpysoem.pxd │ └── pysoem.pyx └── soem │ ├── soem_config.c │ └── soem_config.h └── tests ├── conftest.py ├── foe_testdata ├── random_data_01.bin └── random_data_02.bin ├── pysoem_basic_test.py ├── pysoem_coe_test.py ├── pysoem_foe_test.py ├── pysoem_pdo_test.py ├── pysoem_register_test.py ├── tox_local.ini ├── tox_pypi.ini └── tox_test_pypi.ini /.github/workflows/manual_triggerd_build_and_upload_to_test_pypi.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Build PySOEM sdist and wheels and upload them to TestPyPI 3 | 4 | # This workflow can only be manually triggered using the UI. 5 | on: workflow_dispatch 6 | 7 | jobs: 8 | build_wheels: 9 | name: Build wheels on ${{ matrix.os }} 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | matrix: 13 | os: [ubuntu-22.04, windows-2022, macos-13] 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | submodules: recursive 19 | 20 | - name: Build wheels 21 | uses: pypa/cibuildwheel@v2.23.3 22 | 23 | - uses: actions/upload-artifact@v4 24 | with: 25 | name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} 26 | path: ./wheelhouse/*.whl 27 | 28 | build_sdist: 29 | name: Build source distribution 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v4 33 | with: 34 | submodules: recursive 35 | 36 | - name: Build sdist 37 | run: pipx run build --sdist 38 | 39 | - uses: actions/upload-artifact@v4 40 | with: 41 | name: cibw-sdist 42 | path: ./dist/*.tar.gz 43 | 44 | upload_pypi: 45 | needs: [build_wheels, build_sdist] 46 | runs-on: ubuntu-latest 47 | environment: 48 | name: testpypi 49 | url: https://test.pypi.org/p/pysoem 50 | permissions: 51 | id-token: write # IMPORTANT: this permission is mandatory for trusted publishing 52 | 53 | steps: 54 | - uses: actions/download-artifact@v4 55 | with: 56 | # unpacks all CIBW artifacts into dist/ 57 | pattern: cibw-* 58 | path: dist 59 | merge-multiple: true 60 | 61 | - uses: pypa/gh-action-pypi-publish@release/v1 62 | with: 63 | repository-url: https://test.pypi.org/legacy/ -------------------------------------------------------------------------------- /.github/workflows/release_triggerd_build_and_upload_to_pypi.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Build PySOEM sdist and wheels and upload them to PyPI 3 | 4 | # This is triggered upon creating a release on GitHub. 5 | on: 6 | release: 7 | types: [published] 8 | 9 | jobs: 10 | build_wheels: 11 | name: Build wheels on ${{ matrix.os }} 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | os: [ubuntu-22.04, windows-2022, macos-13] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | submodules: recursive 21 | 22 | - name: Build wheels 23 | uses: pypa/cibuildwheel@v2.23.3 24 | 25 | - uses: actions/upload-artifact@v4 26 | with: 27 | name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} 28 | path: ./wheelhouse/*.whl 29 | 30 | build_sdist: 31 | name: Build source distribution 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v4 35 | with: 36 | submodules: recursive 37 | 38 | - name: Build sdist 39 | run: pipx run build --sdist 40 | 41 | - uses: actions/upload-artifact@v4 42 | with: 43 | name: cibw-sdist 44 | path: ./dist/*.tar.gz 45 | 46 | upload_pypi: 47 | needs: [build_wheels, build_sdist] 48 | runs-on: ubuntu-latest 49 | environment: 50 | name: pypi 51 | url: https://pypi.org/p/pysoem 52 | permissions: 53 | id-token: write # IMPORTANT: this permission is mandatory for trusted publishing 54 | 55 | steps: 56 | - uses: actions/download-artifact@v4 57 | with: 58 | # unpacks all CIBW artifacts into dist/ 59 | pattern: cibw-* 60 | path: dist 61 | merge-multiple: true 62 | 63 | - uses: pypa/gh-action-pypi-publish@release/v1 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # General 2 | venv/ 3 | output/ 4 | 5 | # Build and package files 6 | **/*.egg-info/ 7 | **/build/**/* 8 | _build/ 9 | *.class 10 | *.py[cod] 11 | node_modules/ 12 | dist/ 13 | *.jar 14 | src/pysoem/pysoem.c 15 | 16 | # Log files 17 | **/log/**/* 18 | *.log 19 | 20 | # IDEs 21 | .idea/ 22 | .vscode/ 23 | 24 | # Test settings 25 | tests/pytest.ini 26 | 27 | # Virtual environments 28 | .venv 29 | .conda 30 | 31 | # Generated by OS 32 | .DS_Store 33 | Thumbs.db 34 | 35 | # Applications 36 | *.app 37 | *.exe 38 | *.war 39 | 40 | # Large media files 41 | *.mp4 42 | *.tiff 43 | *.avi 44 | *.flv 45 | *.mov 46 | *.wmv 47 | 48 | # Local development files 49 | .env -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "soem"] 2 | path = soem 3 | url = https://github.com/bnjmnp/SOEM.git 4 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.11" 7 | 8 | submodules: 9 | include: all 10 | 11 | sphinx: 12 | configuration: docs/source/conf.py 13 | 14 | python: 15 | install: 16 | - requirements: docs/requirements.txt 17 | - method: pip 18 | path: . -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 bnjmnp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | include examples/*.py 4 | include src/pysoem/cpysoem.pxd 5 | include src/pysoem/pysoem.pyx 6 | include src/pysoem/pysoem.c 7 | include src/soem/soem_config.c 8 | include src/soem/soem_config.h 9 | recursive-include soem *.h *.c 10 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | PySOEM 2 | ====== 3 | 4 | PySOEM is a Cython wrapper for the Simple Open EtherCAT Master Library (https://github.com/OpenEtherCATsociety/SOEM). 5 | 6 | Introduction 7 | ------------ 8 | 9 | PySOEM enables basic system testing of EtherCAT slave devices with Python. 10 | 11 | Features 12 | 13 | * input process data read and output process data write 14 | * SDO read and write 15 | * EEPROM read and write 16 | * FoE read and write 17 | 18 | Todo 19 | 20 | * EoE 21 | 22 | Beware that real-time applications need some special considerations. 23 | 24 | Requirements 25 | ------------ 26 | 27 | Linux 28 | ^^^^^ 29 | 30 | * Python 3 31 | * Python scripts that use PySOEM must be executed under administrator privileges 32 | 33 | Windows 34 | ^^^^^^^ 35 | 36 | * Python 3 / 64 Bit 37 | * `Npcap `_ [*]_ or `WinPcap `_ 38 | 39 | .. [*] Make sure you check "Install Npcap in WinPcap API-compatible Mode" during the install 40 | 41 | macOS (new with PySOEM 1.1.5) 42 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 43 | 44 | * Python 3 45 | 46 | Installation 47 | ------------ 48 | :: 49 | 50 | python -m pip install pysoem 51 | 52 | or 53 | 54 | :: 55 | 56 | pip install pysoem 57 | 58 | Consider using a `virtualenv `_. 59 | 60 | 61 | Usage 62 | ----- 63 | Although there are some pieces missing, the documentation is hosted on "Read the Docs" at: `pysoem.readthedocs.io `_. 64 | 65 | Please also have a look at `the examples on GitHub `_. 66 | 67 | Contribution 68 | ------------ 69 | 70 | Any contributions are welcome and highly appreciated. 71 | Let's discuss any (major) API change, or large piles of new code first. 72 | Using `this pysoem chat room on gitter `_ is one communication channel option. 73 | 74 | 75 | Changes 76 | ------- 77 | 78 | v1.1.11 79 | ^^^^^^^ 80 | * Adds No-GIL support. 81 | 82 | * Per global setting ``pysoem.settings.always_release_gil``. 83 | * Per Master instance attribute ``always_release_gil``. 84 | * Per function argument ``release_gil``. 85 | 86 | v1.1.10 87 | ^^^^^^^ 88 | * Adds ``pysoem.settings.timeouts`` to configure low-level timeouts at run-time. 89 | 90 | v1.1.9 91 | ^^^^^^^ 92 | * Adds protection against closed network interface connection. 93 | 94 | v1.1.8 95 | ^^^^^^^ 96 | * Fixes null pointer issues when reading not initialized properties ``config_func`` and ``setup_func``. 97 | 98 | v1.1.7 99 | ^^^^^^^ 100 | * Adds ``add_emergency_callback()`` to allow a better handling of emergency messages. 101 | * Improves auto-completion. 102 | 103 | v1.1.6 104 | ^^^^^^^ 105 | * Adds working counter check on SDO read and write. 106 | * Fixes issues with ``config_init()`` when it's called multiple times. 107 | 108 | v1.1.5 109 | ^^^^^^^ 110 | * Adds support for redundancy mode, ``master.open()`` provides now an optional second parameter for the redundancy port. 111 | 112 | v1.1.4 113 | ^^^^^^^ 114 | * Fixes Cython compiling issues. 115 | 116 | v1.1.3 117 | ^^^^^^^ 118 | * Adds function ``_disable_complete_access()`` that stops config_map() from using "complete access" for SDO requests. 119 | 120 | v1.1.0 121 | ^^^^^^^ 122 | * Changed the data type for the ``name`` attribute of SDO info CdefCoeObject and CdefCoeObjectEntry, they are of type bytes now instead of a regular Python 3 string. 123 | * Also changed the ``desc`` attribute of the ``find_adapters()`` list elements to ``bytes``. 124 | * Introduces the ``open()`` context manager function. 125 | * Adds the ``setup_func`` that will maybe later replace the ``config_func``. 126 | 127 | v1.0.8 128 | ^^^^^^^ 129 | * Version bump only to re-upload to PyPI with windows-wheel for Python 3.11 130 | 131 | v1.0.7 132 | ^^^^^^^ 133 | * Fix issues with timeouts at ``amend_mbx`` and ``set_watchdog``. 134 | 135 | v1.0.6 136 | ^^^^^^^ 137 | * Introduces ``amend_mbx`` and ``set_watchdog``, though this is rather experimental 138 | * New example ``firmware_update.py``. 139 | 140 | v1.0.5 141 | ^^^^^^^ 142 | * Introduces the ``manual_state_change`` property 143 | 144 | v1.0.4 145 | ^^^^^^^ 146 | * Proper logging 147 | * Introduces ``mbx_receive`` 148 | 149 | v1.0.3 150 | ^^^^^^^ 151 | * Fix the FoE password issue 152 | 153 | v1.0.2 154 | ^^^^^^^ 155 | * Licence change to MIT licence 156 | * Introduces configurable timeouts for SDO read and SDO write 157 | * Improved API docs 158 | 159 | v1.0.1 160 | ^^^^^^^ 161 | * API change: remove the size parameter for ``foe_write`` 162 | * Introduces overlap map support 163 | 164 | v1.0.0 165 | ^^^^^^^ 166 | * No Cython required to install the package from the source distribution 167 | 168 | v0.1.1 169 | ^^^^^^^ 170 | * Introduces FoE 171 | 172 | v0.1.0 173 | ^^^^^^^ 174 | * Update of the underlying SOEM 175 | 176 | v0.0.18 177 | ^^^^^^^ 178 | * Fixes bug when Ibytes = 0 and Ibits > 0 179 | 180 | v0.0.17 181 | ^^^^^^^ 182 | * Exposes ec_DCtime (``dc_time``) for DC synchronization 183 | 184 | v0.0.16 185 | ^^^^^^^ 186 | * Improvement on SDO Aborts 187 | 188 | v0.0.15 189 | ^^^^^^^ 190 | * SDO info read 191 | 192 | v0.0.14 193 | ^^^^^^^ 194 | * Readme update only 195 | 196 | v0.0.13 197 | ^^^^^^^ 198 | * Initial publication 199 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/developer_notes.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | Developer Notes 3 | =============== 4 | Helper Makefile 5 | --------------- 6 | .. code-block:: makefile 7 | 8 | include .env 9 | 10 | build: 11 | python -m pip install build 12 | python -m build 13 | 14 | clean: 15 | -rm src/pysoem/pysoem.c 16 | -rm src/pysoem/*.pyd 17 | -rm -rf src/pysoem.egg-info 18 | -rm -rf src/pysoem/__pycache__ 19 | -rm -rf src/__pycache__ 20 | -rm -rf tests/__pycache__ 21 | -rm -rf build 22 | -rm -rf dist 23 | -rm -rf .pytest_cache 24 | 25 | uninstall: 26 | python -m pip uninstall -y pysoem 27 | 28 | install_local: 29 | python -m pip install . 30 | 31 | install_testpypi: 32 | python -m pip install -i https://test.pypi.org/simple/ pysoem 33 | 34 | install_pypi: 35 | python -m pip install pysoem 36 | 37 | test: 38 | python -m pip install pytest 39 | pytest tests --ifname=$(IFACE) 40 | 41 | tox_local: 42 | python -m pip install tox 43 | python -m tox run -r -c tests/tox_local.ini -- --ifname=$(IFACE) 44 | 45 | tox_test_pypi: 46 | python -m pip install tox 47 | python -m tox run -r -c tests/tox_test_pypi.ini -- --ifname=$(IFACE) 48 | 49 | tox_pypi: 50 | python -m pip install tox 51 | python -m tox run -r -c tests/tox_pypi.ini -- --ifname=$(IFACE) 52 | 53 | run_basic_example: 54 | python examples/basic_example.py $(IFACE) 55 | 56 | Important notice, for the indentation tabs must be used! 57 | 58 | For the targets that need hardware you need to specify the adapter-id via the ``IFACE`` variable ether 59 | 60 | * during the the make call like: ``make IFACE=`` or 61 | * by creating an environment variable with the same name or 62 | * ``IFACE`` is put into an ``.env`` file which is already imported by the above makefile. For example the ``.env`` should look like this: 63 | .. code-block:: makefile 64 | 65 | IFACE= 66 | 67 | Running Tests 68 | ------------- 69 | 70 | TODO: Add information about the hardware setup for testing. 71 | 72 | Because the tests require hardware they certainly cannot be run on a CI/CD system. 73 | 74 | To run tests with the currently active Python distribution use: 75 | :: 76 | 77 | python -m pytest --ifname=" 78 | 79 | To run the tests locally for all specified Python versions, independent of the operating system run `tox `_ inside the test directory. 80 | 81 | * Use ``tox run -c tox_local.ini -- --ifname=""`` to test pysoem build locally. 82 | * Use ``tox run -c tox_test_pypi.ini -- --ifname=""`` to test pysoem downloaded from TestPyPI. 83 | * Use ``tox run -c tox_pypi.ini -- --ifname=""`` to test pysoem downloaded from PyPI. 84 | 85 | Python versions not installed on your machine will be skipped. -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | cython 2 | sphinx-rtd-theme -------------------------------------------------------------------------------- /docs/source/basics.rst: -------------------------------------------------------------------------------- 1 | ====== 2 | Basics 3 | ====== 4 | 5 | Obtaining a Device Instance 6 | --------------------------- 7 | 8 | Like in the example from the beginning, accessing a device in a network is done by a Masters :py:attr:`pysoem.Master.slaves` list. 9 | 10 | 11 | .. code-block:: python 12 | 13 | import pysoem 14 | 15 | master = pysoem.Master() 16 | 17 | master.open('Your network adapters ID') 18 | 19 | if master.config_init() > 0: 20 | device_foo = master.slaves[0] 21 | device_bar = master.slaves[1] 22 | else: 23 | print('no device found') 24 | 25 | master.close() 26 | 27 | With the device reference you can access some information that was read out from the device during :py:func:`pysoem.Master.config_init`. 28 | For example the devices names: 29 | 30 | .. code-block:: python 31 | 32 | print(device_foo.name) 33 | print(device_bar.name) 34 | 35 | You can also read and wirte CoE objects, and read input process data and wirte output process data, with the device reference. 36 | This will be covered in the next sections. -------------------------------------------------------------------------------- /docs/source/cdef_slave.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | CdefSlave 3 | ========= 4 | 5 | .. autoclass:: pysoem.CdefSlave 6 | :members: 7 | :inherited-members: -------------------------------------------------------------------------------- /docs/source/coe_objects.rst: -------------------------------------------------------------------------------- 1 | =============================== 2 | Reading and Writing CoE Objects 3 | =============================== 4 | 5 | Although reading and writing CoE objects is pretty straight forward, 6 | type conversions are sometimes an issue. 7 | Usually CoE objects are of type boolean, integer, float or string, 8 | but thees types do not map 1:1 to there Python equivalent. 9 | Because of that, reading and writing CoE objects is done on a raw binary basis, 10 | using the built-in :py:class:`bytes`. 11 | That this the same type one would get when reading a file in binary mode. 12 | 13 | .. TODO: cover the 256 byte limit when reading bytes! 14 | 15 | CoE object entries of type string 16 | --------------------------------- 17 | 18 | If the CoE object to be read is a string like the standard object entry 0x1008:00 (Device Name), 19 | one can use the bytes :py:meth:`bytes.decode` method to convert the returned bytes 20 | of :py:meth:`~pysoem.CdefSlave.sdo_read` to a Python 3 string. 21 | 22 | .. code-block:: python 23 | 24 | device_name = device.sdo_read(0x1008, 0).decode('utf-8') 25 | 26 | Although it is only needed occasionally the other way around would be: 27 | 28 | .. code-block:: python 29 | 30 | device.sdo_write(0x2345, 0, 'hello world'.encode('ascii')) 31 | 32 | CoE object entries of type integer 33 | ---------------------------------- 34 | 35 | For integer types a similar approach is possible. 36 | Here is an example on how to read the standard object entry 0x1018:01 (Vendor ID), which is a 32 bit unsigned integer, and convert it to an Python built-in :py:class:`int` using :py:meth:`int.from_bytes`. 37 | 38 | .. code-block:: python 39 | 40 | vendor_id = int.from_bytes(device.sdo_read(0x1018, 1), byteorder='little', signed=False) 41 | 42 | When writing to a 32 bit unsigned integer CoE object entry, :py:meth:`int.to_bytes` could be used like this: 43 | 44 | .. code-block:: python 45 | 46 | device.sdo_write(0x3456, 0, (1).to_bytes(4, byteorder='little', signed=False)) 47 | 48 | Notice that when using to_bytes, the number of bytes must be given as first parameter. 49 | 50 | Using the Python standard library :py:mod:`ctypes` module 51 | --------------------------------------------------------- 52 | 53 | The cytpes types are a bit more flexible, and can also be used for float and boolean types. 54 | Taken the int.from_bytes example from above, the equivalent using ctypes would be: 55 | 56 | .. code-block:: python 57 | 58 | import ctypes 59 | 60 | ... 61 | vendor_id = ctypes.c_uint32.from_buffer_copy(device.sdo_read(0x1018, 1)).value 62 | 63 | Here :py:meth:`~ctypes._CData.from_buffer_copy` was used, which is available for all ctypes types. 64 | 65 | Doing a write to an 32 bit unsigned integer CoE object entry looks a lot nicer in contrast to the int.to_bytes example: 66 | 67 | .. code-block:: python 68 | 69 | device.sdo_write(0x3456, 0, bytes(ctypes.c_uint32(1))) 70 | 71 | .. TODO: Beware that this approach works only for little endian machines, as the "byteorder" cannot be given like in the int.from_bytes / int.to_bytes approach. 72 | 73 | Note that in the ESI file that comes with an EtherCAT slave, some special PLC types are used that might be unusual to some people. 74 | 75 | ======== =========== 76 | ESI Type ctypes Type 77 | ======== =========== 78 | SINT c_int8 79 | INT c_int16 80 | DINT c_int32 81 | LINT c_int64 82 | USINT c_uint8 83 | UINT c_uint16 84 | UDINT c_uint32 85 | ULINT c_uint64 86 | REAL c_float 87 | BOOL c_bool 88 | ======== =========== 89 | 90 | .. Beware that in the PLC world a BOOL is supposed to use 1 bit, whereas C uses usually 1 byte. 91 | 92 | Using the :py:mod:`struct` module 93 | --------------------------------- 94 | 95 | Second alternative to use over the int.from_bytes approach could be the struct module. 96 | 97 | .. code-block:: python 98 | 99 | import struct 100 | 101 | ... 102 | vendor_id = struct.unpack('I', device.sdo_read(0x1018, 1))[0] 103 | 104 | As :py:func:`struct.unpack` returns always a tuple, we need to index the firs element. 105 | The `formate character `_ ``I`` is there to tell the unpack function that we want to convert to a 32 bit unsigned integer. 106 | In the other direction :py:func:`struct.pack` is used: 107 | 108 | .. code-block:: python 109 | 110 | device.sdo_write(0x3456, 0, struct.pack('I', 1)) 111 | 112 | The following list maps the types from the ESI file to the appropriate formate character. 113 | 114 | .. TODO: When using struct there might be some issues with alignment. 115 | 116 | ======== ================ 117 | ESI Type Format Character 118 | ======== ================ 119 | SINT b 120 | INT h 121 | DINT i 122 | LINT q 123 | USINT B 124 | UINT H 125 | UDINT I 126 | ULINT Q 127 | REAL f 128 | BOOL c 129 | ======== ================ 130 | 131 | .. Beware that in the PLC world a BOOL is supposed to use 1 bit, whereas C uses usually 1 byte. 132 | 133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | 15 | on_rtd = os.environ.get('READTHEDOCS') == 'True' 16 | 17 | if on_rtd: 18 | import pysoem 19 | release = pysoem.__version__ 20 | else: 21 | import sys 22 | sys.path.insert(0, os.path.abspath('../../')) 23 | release = '0.0.0' 24 | 25 | 26 | # -- Project information ----------------------------------------------------- 27 | 28 | project = 'PySOEM' 29 | author = 'Benjamin Partzsch' 30 | copyright = '2023, Benjamin Partzsch' 31 | 32 | # The full version, including alpha/beta/rc tags 33 | 34 | 35 | # -- General configuration --------------------------------------------------- 36 | 37 | # Add any Sphinx extension module names here, as strings. They can be 38 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 39 | # ones. 40 | extensions = [ 41 | 'sphinx.ext.autodoc', 42 | 'sphinx.ext.napoleon', 43 | 'sphinx.ext.intersphinx', 44 | ] 45 | 46 | # Add any paths that contain templates here, relative to this directory. 47 | templates_path = ['_templates'] 48 | 49 | # List of patterns, relative to source directory, that match files and 50 | # directories to ignore when looking for source files. 51 | # This pattern also affects html_static_path and html_extra_path. 52 | exclude_patterns = [] 53 | 54 | 55 | # -- Options for HTML output ------------------------------------------------- 56 | 57 | # The theme to use for HTML and HTML Help pages. See the documentation for 58 | # a list of builtin themes. 59 | # 60 | html_theme = 'sphinx_rtd_theme' 61 | 62 | # Add any paths that contain custom static files (such as style sheets) here, 63 | # relative to this directory. They are copied after the builtin static files, 64 | # so a file named "default.css" will overwrite the builtin "default.css". 65 | html_static_path = ['_static'] 66 | 67 | intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} -------------------------------------------------------------------------------- /docs/source/exceptions.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Exceptions 3 | ========== 4 | 5 | 6 | .. autoclass:: pysoem.SdoError 7 | :members: 8 | 9 | .. autoclass:: pysoem.SdoInfoError 10 | :members: 11 | 12 | .. autoclass:: pysoem.MailboxError 13 | :members: 14 | 15 | .. autoclass:: pysoem.PacketError 16 | :members: 17 | 18 | .. autoclass:: pysoem.ConfigMapError 19 | :members: 20 | 21 | .. autoclass:: pysoem.EepromError 22 | :members: -------------------------------------------------------------------------------- /docs/source/helpers.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Helpers 3 | ======= 4 | 5 | 6 | .. autofunction:: pysoem.find_adapters 7 | 8 | .. autofunction:: pysoem.al_status_code_to_string -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to PySOEM's documentation! 2 | ================================== 3 | 4 | PySOEM enables basic system testing of EtherCAT slave devices with Python. 5 | 6 | PySOEM is a wrapper around the `Simple Open EtherCAT Master`_ (SOEM). 7 | Unlike plain C Library wrappers, PySOEM tries to provide an API that can already be used in a more pythonic way. 8 | 9 | .. _`Simple Open EtherCAT Master`: https://github.com/OpenEtherCATsociety/SOEM/ 10 | 11 | One of the simplest examples to get a EtherCAT network up looks like this. 12 | 13 | .. code-block:: python 14 | 15 | import pysoem 16 | 17 | master = pysoem.Master() 18 | 19 | master.open('Your network adapters ID') 20 | 21 | if master.config_init() > 0: 22 | for device in master.slaves: 23 | print(f'Found Device {device.name}') 24 | else: 25 | print('no device found') 26 | 27 | master.close() 28 | 29 | With this script the name of every device in the network will be printed. 30 | 31 | .. toctree:: 32 | :maxdepth: 1 33 | :caption: Getting Started 34 | 35 | Requirements 36 | Installation 37 | 38 | .. toctree:: 39 | :maxdepth: 1 40 | :caption: User Guide 41 | 42 | Basics 43 | Reading and Writing CoE Objects 44 | Process Data Exchange 45 | 46 | .. toctree:: 47 | :maxdepth: 1 48 | :caption: API Documentation 49 | 50 | Master 51 | CdefSlave 52 | Exceptions 53 | Helpers 54 | -------------------------------------------------------------------------------- /docs/source/installation.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Installation 3 | ============ 4 | 5 | .. code:: bash 6 | 7 | $ pip install pysoem -------------------------------------------------------------------------------- /docs/source/master.rst: -------------------------------------------------------------------------------- 1 | ====== 2 | Master 3 | ====== 4 | 5 | .. autoclass:: pysoem.Master 6 | :members: 7 | :inherited-members: -------------------------------------------------------------------------------- /docs/source/process_data.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | Process Data Exchange 3 | ===================== -------------------------------------------------------------------------------- /docs/source/requirements.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Requirements 3 | ============ 4 | 5 | Linux 6 | ----- 7 | 8 | * Python 3 9 | * GCC (as pysoem is compiled during the installation) 10 | * Python scripts that use PySOEM must be executed under administrator privileges 11 | 12 | Windows 13 | ------- 14 | 15 | * Python 3 / 64 Bit 16 | * `Npcap `_ [*]_ or `WinPcap `_ 17 | 18 | .. [*] Make sure you check "Install Npcap in WinPcap API-compatible Mode" during the install 19 | -------------------------------------------------------------------------------- /examples/basic_example.py: -------------------------------------------------------------------------------- 1 | """Toggles the state of a digital output on an EL1259. 2 | 3 | Usage: python basic_example.py 4 | 5 | This example expects a physical slave layout according to _expected_slave_layout, seen below. 6 | Timeouts are all given in us. 7 | """ 8 | 9 | import os 10 | import sys 11 | import struct 12 | import time 13 | import threading 14 | import dataclasses 15 | import typing 16 | import argparse 17 | 18 | 19 | import pysoem 20 | 21 | 22 | BECKHOFF_VENDOR_ID = 0x0000_0002 23 | EK1100_PRODUCT_CODE = 0x044C_2C52 24 | EL3002_PRODUCT_CODE = 0x0BBA_3052 25 | EL1259_PRODUCT_CODE = 0x04EB_3052 26 | 27 | 28 | @dataclasses.dataclass 29 | class Device: 30 | name: str 31 | vendor_id: int 32 | product_code: int 33 | config_func: typing.Callable = None 34 | 35 | 36 | class BasicExample: 37 | def __init__(self, ifname, ifname_red): 38 | self._ifname = ifname 39 | self._ifname_red = ifname_red 40 | self._pd_thread_stop_event = threading.Event() 41 | self._ch_thread_stop_event = threading.Event() 42 | self._actual_wkc = 0 43 | self._master = pysoem.Master() 44 | self._master.in_op = False 45 | self._master.do_check_state = False 46 | self._expected_slave_layout = { 47 | 0: Device("EK1100", BECKHOFF_VENDOR_ID, EK1100_PRODUCT_CODE), 48 | 1: Device("EL3002", BECKHOFF_VENDOR_ID, EL3002_PRODUCT_CODE), 49 | 2: Device("EL1259", BECKHOFF_VENDOR_ID, EL1259_PRODUCT_CODE, self.el1259_setup) 50 | } 51 | 52 | def el1259_setup(self, slave_pos): 53 | """Config function that will be called when transitioning from PreOP state to SafeOP state.""" 54 | slave = self._master.slaves[slave_pos] 55 | 56 | # Enable the digital output. 57 | slave.sdo_write(index=0x8001, subindex=2, data=struct.pack("B", 1)) 58 | 59 | # Select rx PDOs. 60 | rx_map_obj = [ 61 | 0x1603, 62 | 0x1607, 63 | 0x160B, 64 | 0x160F, 65 | 0x1611, 66 | 0x1617, 67 | 0x161B, 68 | 0x161F, 69 | 0x1620, 70 | 0x1621, 71 | 0x1622, 72 | 0x1623, 73 | 0x1624, 74 | 0x1625, 75 | 0x1626, 76 | 0x1627, 77 | ] 78 | rx_map_obj_bytes = struct.pack( 79 | "Bx" + "".join(["H" for _ in range(len(rx_map_obj))]), len(rx_map_obj), *rx_map_obj) 80 | slave.sdo_write(index=0x1C12, subindex=0, data=rx_map_obj_bytes, ca=True) 81 | 82 | def _processdata_thread(self): 83 | """Background thread that sends and receives the process-data frame in a 10ms interval.""" 84 | while not self._pd_thread_stop_event.is_set(): 85 | self._master.send_processdata() 86 | self._actual_wkc = self._master.receive_processdata(timeout=100_000) 87 | if not self._actual_wkc == self._master.expected_wkc: 88 | print("incorrect wkc") 89 | time.sleep(0.01) 90 | 91 | def _pdo_update_loop(self): 92 | """The actual application code used to toggle the digital output at the EL1259 in an endless loop. 93 | 94 | Called when all slaves reached OP state. 95 | Updates the rx PDO of the EL1259 every second. 96 | """ 97 | self._master.in_op = True 98 | 99 | output_len = len(self._master.slaves[2].output) 100 | 101 | tmp = bytearray([0 for i in range(output_len)]) 102 | 103 | toggle = True 104 | try: 105 | while 1: 106 | if toggle: 107 | tmp[0] = 0x00 108 | else: 109 | tmp[0] = 0x02 110 | self._master.slaves[2].output = bytes(tmp) 111 | 112 | toggle ^= True 113 | 114 | time.sleep(1) 115 | 116 | except KeyboardInterrupt: 117 | # ctrl-C abort handling 118 | print("stopped") 119 | 120 | def run(self): 121 | self._master.open(self._ifname, self._ifname_red) 122 | 123 | if not self._master.config_init() > 0: 124 | self._master.close() 125 | raise BasicExampleError("no slave found") 126 | 127 | for i, slave in enumerate(self._master.slaves): 128 | if not ((slave.man == self._expected_slave_layout[i].vendor_id) and 129 | (slave.id == self._expected_slave_layout[i].product_code)): 130 | self._master.close() 131 | raise BasicExampleError("unexpected slave layout") 132 | slave.config_func = self._expected_slave_layout[i].config_func 133 | slave.is_lost = False 134 | 135 | self._master.config_map() 136 | 137 | if self._master.state_check(pysoem.SAFEOP_STATE, timeout=50_000) != pysoem.SAFEOP_STATE: 138 | self._master.close() 139 | raise BasicExampleError("not all slaves reached SAFEOP state") 140 | 141 | slave.dc_sync(act=True, sync0_cycle_time=10_000_000) # time is given in ns -> 10,000,000ns = 10ms 142 | 143 | self._master.state = pysoem.OP_STATE 144 | 145 | check_thread = threading.Thread(target=self._check_thread) 146 | check_thread.start() 147 | proc_thread = threading.Thread(target=self._processdata_thread) 148 | proc_thread.start() 149 | 150 | # send one valid process data to make outputs in slaves happy 151 | self._master.send_processdata() 152 | self._master.receive_processdata(timeout=2000) 153 | # request OP state for all slaves 154 | 155 | self._master.write_state() 156 | 157 | all_slaves_reached_op_state = False 158 | for i in range(40): 159 | self._master.state_check(pysoem.OP_STATE, timeout=50_000) 160 | if self._master.state == pysoem.OP_STATE: 161 | all_slaves_reached_op_state = True 162 | break 163 | 164 | if all_slaves_reached_op_state: 165 | self._pdo_update_loop() 166 | 167 | self._pd_thread_stop_event.set() 168 | self._ch_thread_stop_event.set() 169 | proc_thread.join() 170 | check_thread.join() 171 | self._master.state = pysoem.INIT_STATE 172 | # request INIT state for all slaves 173 | self._master.write_state() 174 | self._master.close() 175 | 176 | if not all_slaves_reached_op_state: 177 | raise BasicExampleError("not all slaves reached OP state") 178 | 179 | @staticmethod 180 | def _check_slave(slave, pos): 181 | if slave.state == (pysoem.SAFEOP_STATE + pysoem.STATE_ERROR): 182 | print(f"ERROR : slave {pos} is in SAFE_OP + ERROR, attempting ack.") 183 | slave.state = pysoem.SAFEOP_STATE + pysoem.STATE_ACK 184 | slave.write_state() 185 | elif slave.state == pysoem.SAFEOP_STATE: 186 | print(f"WARNING : slave {pos} is in SAFE_OP, try change to OPERATIONAL.") 187 | slave.state = pysoem.OP_STATE 188 | slave.write_state() 189 | elif slave.state > pysoem.NONE_STATE: 190 | if slave.reconfig(): 191 | slave.is_lost = False 192 | print(f"MESSAGE : slave {pos} reconfigured") 193 | elif not slave.is_lost: 194 | slave.state_check(pysoem.OP_STATE) 195 | if slave.state == pysoem.NONE_STATE: 196 | slave.is_lost = True 197 | print(f"ERROR : slave {pos} lost") 198 | if slave.is_lost: 199 | if slave.state == pysoem.NONE_STATE: 200 | if slave.recover(): 201 | slave.is_lost = False 202 | print(f"MESSAGE : slave {pos} recovered") 203 | else: 204 | slave.is_lost = False 205 | print(f"MESSAGE : slave {pos} found") 206 | 207 | def _check_thread(self): 208 | while not self._ch_thread_stop_event.is_set(): 209 | if self._master.in_op and ((self._actual_wkc < self._master.expected_wkc) or self._master.do_check_state): 210 | self._master.do_check_state = False 211 | self._master.read_state() 212 | for i, slave in enumerate(self._master.slaves): 213 | if slave.state != pysoem.OP_STATE: 214 | self._master.do_check_state = True 215 | BasicExample._check_slave(slave, i) 216 | if not self._master.do_check_state: 217 | print("OK : all slaves resumed OPERATIONAL.") 218 | time.sleep(0.01) 219 | 220 | 221 | class BasicExampleError(Exception): 222 | def __init__(self, message): 223 | super().__init__(message) 224 | self.message = message 225 | 226 | 227 | if __name__ == "__main__": 228 | parser = argparse.ArgumentParser(description="Example code for PySOEM.") 229 | parser.add_argument("iface", type=str, help="ID of the network adapter used.") 230 | parser.add_argument("ifname_red", nargs="?", type=str, help="Optional: ID of the second network adapter used (for redundancy).") 231 | args = parser.parse_args() 232 | 233 | try: 234 | BasicExample(args.iface, args.ifname_red).run() 235 | except BasicExampleError as err: 236 | print(f"{os.path.basename(__file__)} failed: {err.message}") 237 | sys.exit(1) 238 | -------------------------------------------------------------------------------- /examples/find_adapters.py: -------------------------------------------------------------------------------- 1 | """Prints name and description of available network adapters.""" 2 | 3 | import pysoem 4 | 5 | 6 | adapters = pysoem.find_adapters() 7 | 8 | for i, adapter in enumerate(adapters): 9 | print('Adapter {}'.format(i)) 10 | print(' {}'.format(adapter.name)) 11 | print(' {}'.format(adapter.desc)) 12 | -------------------------------------------------------------------------------- /examples/firmware_update.py: -------------------------------------------------------------------------------- 1 | """Firmware update example application for PySOEM. 2 | 3 | Note: PySOEM >= 1.0.6 is required. 4 | """ 5 | 6 | import sys 7 | import argparse 8 | import logging 9 | import struct 10 | 11 | import pysoem 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | class FirmwareUpdateError(Exception): 17 | pass 18 | 19 | 20 | def argument_parsing(cmd_line_args): 21 | parser = argparse.ArgumentParser(description=__doc__) 22 | parser.add_argument('interface_name', type=str, 23 | help='ID of the network adapter used for the EtherCAT network.') 24 | parser.add_argument('device_position', type=int, choices=range(1, 2**16), metavar="1..65535", 25 | help='Position of the device in the EtherCAT network to be updated.') 26 | parser.add_argument('update_file', type=argparse.FileType('rb'), 27 | help='Path to the file to be uploaded.') 28 | return parser.parse_args(cmd_line_args) 29 | 30 | 31 | def main(cmd_line_args): 32 | script_args = argument_parsing(cmd_line_args) 33 | 34 | master = pysoem.Master() 35 | master.open(script_args.interface_name) 36 | 37 | logger.info('Enumerate devices in the network..') 38 | number_of_devices_found = master.config_init() 39 | logger.info('..Number of devices found: %d.' % number_of_devices_found) 40 | if number_of_devices_found == 0: 41 | raise FirmwareUpdateError(f'No device found at the given interface: {script_args.interface_name}!') 42 | elif script_args.device_position > number_of_devices_found: 43 | raise FirmwareUpdateError(f'Requested to update device in position {script_args.device_position}, but only {number_of_devices_found} devices are available!') 44 | 45 | device = master.slaves[script_args.device_position-1] 46 | 47 | logger.info('Request Init state for the target device in position %d.' % script_args.device_position) 48 | device.state = pysoem.INIT_STATE 49 | device.write_state() 50 | device.state_check(pysoem.INIT_STATE, 3_000_000) 51 | if device.state != pysoem.INIT_STATE: 52 | raise FirmwareUpdateError('The device did not go into Init state!') 53 | 54 | boot_rx_mbx = device.eeprom_read(pysoem.SiiOffset.BOOT_RX_MBX) 55 | rx_mbx_addr, rx_mbx_len = struct.unpack('HH', boot_rx_mbx) 56 | boot_tx_mbx = device.eeprom_read(pysoem.SiiOffset.BOOT_TX_MBX) 57 | tx_mbx_addr, tx_mbx_len = struct.unpack('HH', boot_tx_mbx) 58 | logger.info('Update SM0: {Address: 0x%4.4x; Length: %4d}' % (rx_mbx_addr, rx_mbx_len)) 59 | device.amend_mbx(mailbox='out', start_address=rx_mbx_addr, size=rx_mbx_len) 60 | logger.info('Update SM1: {Address: 0x%4.4x; Length: %4d}' % (tx_mbx_addr, tx_mbx_len)) 61 | device.amend_mbx(mailbox='in', start_address=tx_mbx_addr, size=tx_mbx_len) 62 | 63 | logger.info('Request Boot state for the device.') 64 | device.state = pysoem.BOOT_STATE 65 | device.write_state() 66 | device.state_check(pysoem.BOOT_STATE, 3_000_000) 67 | if device.state != pysoem.BOOT_STATE: 68 | raise FirmwareUpdateError('The device did not go into Boot state!') 69 | 70 | logger.info('Send file to the device using FoE write.') 71 | device.foe_write(filename=script_args.update_file.name[:-4], 72 | password=0, 73 | data=script_args.update_file.read(), 74 | timeout=6_000_000) 75 | logger.info('Download completed.') 76 | 77 | logger.info('Request Init state for the device.') 78 | device.state = pysoem.INIT_STATE 79 | device.write_state() 80 | 81 | master.close() 82 | logger.info('Finished.') 83 | 84 | 85 | if __name__ == '__main__': 86 | try: 87 | main(sys.argv[1:]) 88 | except Exception as e: 89 | print(e, file=sys.stderr) 90 | sys.exit(1) 91 | -------------------------------------------------------------------------------- /examples/minimal_example.py: -------------------------------------------------------------------------------- 1 | """Prints the analog-to-digital converted voltage of an EL3002. 2 | 3 | Usage: python minimal_example.py 4 | 5 | This example expects a physical slave layout according to 6 | _expected_slave_layout, see below. 7 | """ 8 | 9 | import sys 10 | import struct 11 | import time 12 | import collections 13 | 14 | import pysoem 15 | 16 | 17 | class MinimalExample: 18 | 19 | BECKHOFF_VENDOR_ID = 0x0002 20 | EK1100_PRODUCT_CODE = 0x044c2c52 21 | EL3002_PRODUCT_CODE = 0x0bba3052 22 | 23 | def __init__(self, ifname): 24 | self._ifname = ifname 25 | self._master = pysoem.Master() 26 | SlaveSet = collections.namedtuple( 27 | 'SlaveSet', 'slave_name product_code config_func') 28 | self._expected_slave_mapping = {0: SlaveSet('EK1100', self.EK1100_PRODUCT_CODE, None), 29 | 1: SlaveSet('EL3002', self.EL3002_PRODUCT_CODE, self.el3002_setup)} 30 | 31 | def el3002_setup(self, slave_pos): 32 | slave = self._master.slaves[slave_pos] 33 | 34 | slave.sdo_write(0x1c12, 0, struct.pack('B', 0)) 35 | 36 | map_1c13_bytes = struct.pack('BxHH', 2, 0x1A01, 0x1A03) 37 | slave.sdo_write(0x1c13, 0, map_1c13_bytes, True) 38 | 39 | def run(self): 40 | 41 | self._master.open(self._ifname) 42 | 43 | # config_init returns the number of slaves found 44 | if self._master.config_init() > 0: 45 | 46 | print("{} slaves found and configured".format( 47 | len(self._master.slaves))) 48 | 49 | for i, slave in enumerate(self._master.slaves): 50 | assert(slave.man == self.BECKHOFF_VENDOR_ID) 51 | assert( 52 | slave.id == self._expected_slave_mapping[i].product_code) 53 | slave.config_func = self._expected_slave_mapping[i].config_func 54 | 55 | # PREOP_STATE to SAFEOP_STATE request - each slave's config_func is called 56 | self._master.config_map() 57 | 58 | # wait 50 ms for all slaves to reach SAFE_OP state 59 | if self._master.state_check(pysoem.SAFEOP_STATE, 50000) != pysoem.SAFEOP_STATE: 60 | self._master.read_state() 61 | for slave in self._master.slaves: 62 | if not slave.state == pysoem.SAFEOP_STATE: 63 | print('{} did not reach SAFEOP state'.format(slave.name)) 64 | print('al status code {} ({})'.format(hex(slave.al_status), 65 | pysoem.al_status_code_to_string(slave.al_status))) 66 | raise Exception('not all slaves reached SAFEOP state') 67 | 68 | self._master.state = pysoem.OP_STATE 69 | self._master.write_state() 70 | 71 | self._master.state_check(pysoem.OP_STATE, 50000) 72 | if self._master.state != pysoem.OP_STATE: 73 | self._master.read_state() 74 | for slave in self._master.slaves: 75 | if not slave.state == pysoem.OP_STATE: 76 | print('{} did not reach OP state'.format(slave.name)) 77 | print('al status code {} ({})'.format(hex(slave.al_status), 78 | pysoem.al_status_code_to_string(slave.al_status))) 79 | raise Exception('not all slaves reached OP state') 80 | 81 | try: 82 | while 1: 83 | # free run cycle 84 | self._master.send_processdata() 85 | self._master.receive_processdata(2000) 86 | 87 | volgage_ch_1_el3002_as_bytes = self._master.slaves[1].input 88 | volgage_ch_1_el3002_as_int16 = struct.unpack( 89 | 'hh', volgage_ch_1_el3002_as_bytes)[0] 90 | voltage = volgage_ch_1_el3002_as_int16 * 10 / 0x8000 91 | print('EL3002 Ch 1 PDO: {:#06x}; Voltage: {:.4}'.format( 92 | volgage_ch_1_el3002_as_int16, voltage)) 93 | 94 | time.sleep(1) 95 | 96 | except KeyboardInterrupt: 97 | # ctrl-C abort handling 98 | print('stopped') 99 | 100 | self._master.state = pysoem.INIT_STATE 101 | # request INIT state for all slaves 102 | self._master.write_state() 103 | self._master.close() 104 | else: 105 | print('slaves not found') 106 | 107 | 108 | if __name__ == '__main__': 109 | 110 | print('minimal_example') 111 | 112 | if len(sys.argv) > 1: 113 | try: 114 | MinimalExample(sys.argv[1]).run() 115 | except Exception as expt: 116 | print(expt) 117 | sys.exit(1) 118 | else: 119 | print('usage: minimal_example ifname') 120 | sys.exit(1) 121 | -------------------------------------------------------------------------------- /examples/read_eeprom.py: -------------------------------------------------------------------------------- 1 | """Prints name and description of available network adapters.""" 2 | 3 | import sys 4 | import pysoem 5 | 6 | 7 | def read_eeprom_of_first_slave(ifname): 8 | master = pysoem.Master() 9 | 10 | master.open(ifname) 11 | 12 | if master.config_init() > 0: 13 | 14 | first_slave = master.slaves[0] 15 | 16 | for i in range(0, 0x80, 2): 17 | print('{:04x}:'.format(i), end='') 18 | print('|'.join('{:02x}'.format(x) for x in first_slave.eeprom_read(i))) 19 | 20 | else: 21 | print('no slave available') 22 | 23 | master.close() 24 | 25 | 26 | if __name__ == '__main__': 27 | 28 | print('script started') 29 | 30 | if len(sys.argv) > 1: 31 | read_eeprom_of_first_slave(sys.argv[1]) 32 | else: 33 | print('give ifname as script argument') 34 | -------------------------------------------------------------------------------- /examples/read_sdo_info.py: -------------------------------------------------------------------------------- 1 | """Prints all the SDO info of every slave that supports the SDO info feature""" 2 | 3 | import sys 4 | import pysoem 5 | 6 | 7 | def read_sdo_info(ifname): 8 | master = pysoem.Master() 9 | 10 | master.open(ifname) 11 | 12 | if master.config_init() > 0: 13 | 14 | for slave in master.slaves: 15 | try: 16 | od = slave.od 17 | except pysoem.SdoInfoError: 18 | print('no SDO info for {}'.format(slave.name)) 19 | else: 20 | print(slave.name) 21 | 22 | for obj in od: 23 | print(' Idx: {}; Code: {}; Type: {}; BitSize: {}; Access: {}; Name: "{}"'.format( 24 | hex(obj.index), 25 | obj.object_code, 26 | obj.data_type, 27 | obj.bit_length, 28 | hex(obj.obj_access), 29 | obj.name)) 30 | for i, entry in enumerate(obj.entries): 31 | if entry.data_type > 0 and entry.bit_length > 0: 32 | print(' Subindex {}; Type: {}; BitSize: {}; Access: {} Name: "{}"'.format( 33 | i, 34 | entry.data_type, 35 | entry.bit_length, 36 | hex(entry.obj_access), 37 | entry.name)) 38 | 39 | else: 40 | print('no slave available') 41 | 42 | master.close() 43 | 44 | 45 | if __name__ == '__main__': 46 | 47 | print('script started') 48 | 49 | if len(sys.argv) > 1: 50 | read_sdo_info(sys.argv[1]) 51 | else: 52 | print('give ifname as script argument') 53 | -------------------------------------------------------------------------------- /examples/write_foe.py: -------------------------------------------------------------------------------- 1 | 2 | import sys 3 | 4 | import pysoem 5 | 6 | 7 | def write_file_to_first_slave(ifname, file_path): 8 | master = pysoem.Master() 9 | 10 | master.open(ifname) 11 | 12 | try: 13 | if master.config_init() > 0: 14 | 15 | first_slave = master.slaves[0] 16 | 17 | with open(file_path, 'rb') as file: 18 | file_data = file.read() 19 | first_slave.foe_write('data.bin', 0, file_data) 20 | else: 21 | print('no slave available') 22 | except Exception as ex: 23 | raise ex 24 | finally: 25 | master.close() 26 | 27 | 28 | if __name__ == '__main__': 29 | 30 | print('script started') 31 | 32 | if len(sys.argv) > 1: 33 | write_file_to_first_slave(sys.argv[1], sys.argv[2]) 34 | else: 35 | print('usage: python write_foe.py ') 36 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42", "wheel", "Cython>=0.29.31"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.cibuildwheel] 6 | # Skip building on CPython 3.6 on all platforms - because 3.6 it is causing issues. 7 | skip = "cp36-*" 8 | 9 | # Limit the created wheels for windows to AMD64. 10 | [tool.cibuildwheel.windows] 11 | archs = ["AMD64"] -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import codecs 4 | import re 5 | 6 | from setuptools import setup, find_packages, Extension 7 | 8 | try: 9 | import Cython 10 | except ImportError: 11 | USE_CYTHON = False 12 | else: 13 | from Cython.Build import cythonize 14 | USE_CYTHON = True 15 | 16 | 17 | soem_sources = [] 18 | soem_inc_dirs = [] 19 | 20 | if sys.platform.startswith('win'): 21 | soem_macros = [('WIN32', ''), ('_CRT_SECURE_NO_WARNINGS', '')] 22 | soem_lib_dirs = [os.path.join('.', 'soem', 'oshw', 'win32', 'wpcap', 'Lib', 'x64')] 23 | soem_libs = ['wpcap', 'Packet', 'Ws2_32', 'Winmm'] 24 | soem_inc_dirs.append(os.path.join('.', 'soem', 'oshw', 'win32', 'wpcap', 'Include')) 25 | os_name = 'win32' 26 | elif sys.platform.startswith('linux'): 27 | soem_macros = [] 28 | soem_lib_dirs = [] 29 | soem_libs = ['pthread', 'rt'] 30 | os_name = 'linux' 31 | elif sys.platform.startswith('darwin'): 32 | soem_macros = [] 33 | soem_lib_dirs = [] 34 | soem_libs = ['pthread', 'pcap'] 35 | os_name = 'macosx' 36 | 37 | soem_macros.append(('EC_VER2', '')) 38 | soem_macros.append(('USE_SOEM_CONFIG_H', '')) 39 | 40 | soem_sources.extend([os.path.join('.', 'soem', 'osal', os_name, 'osal.c'), 41 | os.path.join('.', 'soem', 'oshw', os_name, 'oshw.c'), 42 | os.path.join('.', 'soem', 'oshw', os_name, 'nicdrv.c'), 43 | os.path.join('.', 'soem', 'soem', 'ethercatbase.c'), 44 | os.path.join('.', 'soem', 'soem', 'ethercatcoe.c'), 45 | os.path.join('.', 'soem', 'soem', 'ethercatconfig.c'), 46 | os.path.join('.', 'soem', 'soem', 'ethercatdc.c'), 47 | os.path.join('.', 'soem', 'soem', 'ethercatfoe.c'), 48 | os.path.join('.', 'soem', 'soem', 'ethercatmain.c'), 49 | os.path.join('.', 'soem', 'soem', 'ethercatprint.c'), 50 | os.path.join('.', 'soem', 'soem', 'ethercatsoe.c'), 51 | os.path.join('.', 'src', 'soem', 'soem_config.c')]) 52 | 53 | soem_inc_dirs.extend([os.path.join('.', 'soem', 'oshw', os_name), 54 | os.path.join('.', 'soem', 'osal', os_name), 55 | os.path.join('.', 'soem', 'oshw'), 56 | os.path.join('.', 'soem', 'osal'), 57 | os.path.join('.', 'soem', 'soem'), 58 | os.path.join('.', 'src', 'soem')]) 59 | 60 | 61 | def readme(): 62 | """see: http://python-packaging.readthedocs.io/en/latest/metadata.html""" 63 | with open('README.rst') as f: 64 | return f.read() 65 | 66 | 67 | here = os.path.abspath(os.path.dirname(__file__)) 68 | 69 | 70 | def read(*parts): 71 | with codecs.open(os.path.join(here, *parts), 'r') as fp: 72 | return fp.read() 73 | 74 | 75 | def find_version(*file_paths): 76 | version_file = read(*file_paths) 77 | version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", 78 | version_file, re.M) 79 | if version_match: 80 | return version_match.group(1) 81 | raise RuntimeError("Unable to find version string.") 82 | 83 | 84 | ext = '.pyx' if USE_CYTHON else '.c' 85 | 86 | extensions = [ 87 | Extension( 88 | 'pysoem.pysoem', 89 | ['src/pysoem/pysoem'+ext] + soem_sources, 90 | define_macros=soem_macros, 91 | libraries=soem_libs, 92 | library_dirs=soem_lib_dirs, 93 | include_dirs=['./pysoem'] + soem_inc_dirs 94 | ) 95 | ] 96 | 97 | if USE_CYTHON: 98 | from Cython.Build import cythonize 99 | extensions = cythonize(extensions, compiler_directives={"language_level": "2"}) 100 | 101 | setup(name='pysoem', 102 | version=find_version("src", "pysoem", "__init__.py"), 103 | description='Cython wrapper for the SOEM Library', 104 | author='Benjamin Partzsch', 105 | author_email='benjamin_partzsch@web.de', 106 | url='https://github.com/bnjmnp/pysoem', 107 | license='MIT', 108 | long_description=readme(), 109 | ext_modules=extensions, 110 | packages=['pysoem'], 111 | package_dir={"": "src"}, 112 | project_urls={ 113 | 'Documentation': 'https://pysoem.readthedocs.io', 114 | }, 115 | classifiers=[ 116 | 'Development Status :: 2 - Pre-Alpha', 117 | 'License :: OSI Approved :: MIT License', 118 | 'Programming Language :: Python', 119 | 'Programming Language :: Cython', 120 | 'Programming Language :: C', 121 | 'Programming Language :: Python :: 3', 122 | 'Programming Language :: Python :: Implementation :: CPython', 123 | 'Topic :: Scientific/Engineering', 124 | ] 125 | ) 126 | -------------------------------------------------------------------------------- /src/pysoem/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '1.1.12-dev' 2 | 3 | 4 | # Classes: 5 | from pysoem.pysoem import ( 6 | Master, 7 | SdoError, 8 | Emergency, 9 | SdoInfoError, 10 | MailboxError, 11 | PacketError, 12 | ConfigMapError, 13 | EepromError, 14 | WkcError, 15 | NetworkInterfaceNotOpenError, 16 | SiiOffset, 17 | ) 18 | 19 | # State constants: 20 | from pysoem.pysoem import ( 21 | NONE_STATE, 22 | INIT_STATE, 23 | PREOP_STATE, 24 | BOOT_STATE, 25 | SAFEOP_STATE, 26 | OP_STATE, 27 | STATE_ACK, 28 | STATE_ERROR, 29 | ) 30 | 31 | # ECT constants: 32 | from pysoem.pysoem import ( 33 | ECT_REG_WD_DIV, 34 | ECT_REG_WD_TIME_PDI, 35 | ECT_REG_WD_TIME_PROCESSDATA, 36 | ECT_REG_SM0, 37 | ECT_REG_SM1, 38 | ECT_COEDET_SDO, 39 | ECT_COEDET_SDOINFO, 40 | ECT_COEDET_PDOASSIGN, 41 | ECT_COEDET_PDOCONFIG, 42 | ECT_COEDET_UPLOAD, 43 | ECT_COEDET_SDOCA, 44 | ) 45 | globals().update(pysoem.ec_datatype.__members__) 46 | 47 | # Functions: 48 | from pysoem.pysoem import ( 49 | find_adapters, 50 | open, 51 | al_status_code_to_string, 52 | ) 53 | 54 | # Raw Cdefs: 55 | from pysoem.pysoem import ( 56 | CdefMaster, 57 | CdefSlave, 58 | CdefCoeObjectEntry, 59 | ) 60 | 61 | # Settings: 62 | from pysoem.pysoem import ( 63 | settings 64 | ) 65 | -------------------------------------------------------------------------------- /src/pysoem/cpysoem.pxd: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Benjamin Partzsch 2 | # 3 | # This file is part of the PySOEM project and licenced under the MIT license. 4 | # Check the license terms in the LICENSE file. 5 | # 6 | # PySOEM is a Cython wrapper for the Simple Open EtherCAT Master (SOEM) library 7 | # (https://github.com/OpenEtherCATsociety/SOEM). 8 | # 9 | # EtherCAT is a registered trademark of Beckhoff Automation GmbH. 10 | # 11 | # 12 | """PySOEM is a Cython wrapper for the SOEM library.""" 13 | 14 | # 15 | # This creates a helper library for PySOEM, to be used with `cimport cpysoem` 16 | # 17 | 18 | from libc.stdint cimport int8_t, int16_t, int32_t, int64_t, uint8_t, uint16_t, uint32_t, uint64_t 19 | 20 | cdef extern from "ethercat.h": 21 | 22 | cdef enum: 23 | EC_MAXBUF = 16 24 | EC_MAXMBX = 1486 25 | EC_BUFSIZE = 1518 26 | 27 | ec_adaptert* ec_find_adapters() 28 | 29 | # from osal.h 30 | 31 | ctypedef int8_t boolean 32 | ctypedef int8_t int8 33 | ctypedef int16_t int16 34 | ctypedef int32_t int32 35 | ctypedef int64_t int64 36 | ctypedef uint8_t uint8 37 | ctypedef uint16_t uint16 38 | ctypedef uint32_t uint32 39 | ctypedef uint64_t uint64 40 | ctypedef float float32 41 | ctypedef double float64 42 | ctypedef uint8 ec_mbxbuft[EC_MAXMBX] 43 | 44 | ctypedef uint8 ec_bufT[EC_BUFSIZE] 45 | 46 | ctypedef struct ec_timet: 47 | uint32 sec 48 | uint32 usec 49 | 50 | # from ethercattype.h 51 | 52 | ctypedef enum ec_err_type: 53 | EC_ERR_TYPE_SDO_ERROR = 0 54 | EC_ERR_TYPE_EMERGENCY = 1 55 | EC_ERR_TYPE_PACKET_ERROR = 3 56 | EC_ERR_TYPE_SDOINFO_ERROR = 4 57 | EC_ERR_TYPE_FOE_ERROR = 5 58 | EC_ERR_TYPE_FOE_BUF2SMALL = 6 59 | EC_ERR_TYPE_FOE_PACKETNUMBER = 7 60 | EC_ERR_TYPE_SOE_ERROR = 8 61 | EC_ERR_TYPE_MBX_ERROR = 9 62 | EC_ERR_TYPE_FOE_FILE_NOTFOUND = 10 63 | 64 | ctypedef enum ec_state: 65 | EC_STATE_NONE = 0x00 66 | EC_STATE_INIT = 0x01 67 | EC_STATE_PRE_OP = 0x02 68 | EC_STATE_BOOT = 0x03 69 | EC_STATE_SAFE_OP = 0x04 70 | EC_STATE_OPERATIONAL = 0x08 71 | EC_STATE_ACK = 0x10 72 | EC_STATE_ERROR = 0x10 73 | 74 | ctypedef struct ec_errort: 75 | ec_timet Time 76 | boolean Signal 77 | uint16 Slave 78 | uint16 Index 79 | uint8 SubIdx 80 | ec_err_type Etype 81 | # union - General abortcode 82 | int32 AbortCode 83 | # union - Specific error for Emergency mailbox 84 | uint16 ErrorCode 85 | uint8 ErrorReg 86 | uint8 b1 87 | uint16 w1 88 | uint16 w2 89 | 90 | # from soem_config.h 91 | ctypedef struct Ttimeouts: 92 | int ret 93 | int safe 94 | int eeprom 95 | int tx_mailbox 96 | int rx_mailbox 97 | int state 98 | extern Ttimeouts soem_timeouts; 99 | 100 | # from nicdrv.h 101 | 102 | ctypedef struct ec_stackT: 103 | int *sock 104 | ec_bufT *(*txbuf) #[EC_MAXBUF] 105 | int *(*txbuflength) #[EC_MAXBUF] 106 | ec_bufT *tempbuf 107 | ec_bufT *(*rxbuf) #[EC_MAXBUF] 108 | int *(*rxbufstat) #[EC_MAXBUF] 109 | int *(*rxsa) #[EC_MAXBUF] 110 | 111 | ctypedef struct ecx_redportt: 112 | ec_stackT stack 113 | int sockhandle 114 | ec_bufT *rxbuf #[EC_MAXBUF] 115 | int *rxbufstat #[EC_MAXBUF] 116 | int *rxsa #[EC_MAXBUF] 117 | ec_bufT tempinbuf 118 | 119 | ctypedef struct ecx_portt: 120 | pass 121 | 122 | # from eethercatmain.h 123 | 124 | ctypedef struct ec_adaptert: 125 | char* name 126 | char* desc 127 | ec_adaptert* next 128 | 129 | ctypedef struct ec_fmmut: 130 | uint32 LogStart 131 | uint16 LogLength 132 | uint8 LogStartbit 133 | uint8 LogEndbit 134 | uint16 PhysStart 135 | uint8 PhysStartBit 136 | uint8 FMMUtype 137 | uint8 FMMUactive 138 | uint8 unused1 139 | uint16 unused2 140 | 141 | ctypedef struct ec_smt: 142 | uint16 StartAddr 143 | uint16 SMlength 144 | uint32 SMflags 145 | 146 | ctypedef struct ec_slavet: 147 | uint16 state 148 | uint16 ALstatuscode 149 | uint16 configadr 150 | uint16 aliasadr 151 | uint32 eep_man 152 | uint32 eep_id 153 | uint32 eep_rev 154 | uint16 Itype 155 | uint16 Dtype 156 | uint16 Obits 157 | uint32 Obytes 158 | uint8 *outputs 159 | uint8 Ostartbit 160 | uint16 Ibits 161 | uint32 Ibytes 162 | uint8 *inputs 163 | uint8 Istartbit 164 | ec_smt *SM #[EC_MAXSM] 165 | uint8 *SMtype #[EC_MAXSM] 166 | ec_fmmut *FMMU #[EC_MAXFMMU] 167 | uint8 FMMU0func 168 | uint8 FMMU1func 169 | uint8 FMMU2func 170 | uint8 FMMU3func 171 | uint16 mbx_l 172 | uint16 mbx_wo 173 | uint16 mbx_rl 174 | uint16 mbx_ro 175 | uint16 mbx_proto 176 | uint8 mbx_cnt 177 | boolean hasdc 178 | uint8 ptype 179 | uint8 topology 180 | uint8 activeports 181 | uint8 consumedports 182 | uint16 parent 183 | uint8 parentport 184 | uint8 entryport 185 | int32 DCrtA 186 | int32 DCrtB 187 | int32 DCrtC 188 | int32 DCrtD 189 | int32 pdelay 190 | uint16 DCnext 191 | uint16 DCprevious 192 | int32 DCcycle 193 | int32 DCshift 194 | uint8 DCactive 195 | uint16 configindex 196 | uint16 SIIindex 197 | uint8 eep_8byte 198 | uint8 eep_pdi 199 | uint8 CoEdetails 200 | uint8 FoEdetails 201 | uint8 EoEdetails 202 | uint8 SoEdetails 203 | int16 Ebuscurrent 204 | uint8 blockLRW 205 | uint8 group 206 | uint8 FMMUunused 207 | boolean islost 208 | int (*PO2SOconfig)(uint16 slave, void* user) 209 | int (*PO2SOconfigx)(ecx_contextt* context, uint16 slave) 210 | void* user 211 | char *name #[EC_MAXNAME + 1] 212 | 213 | ctypedef struct ec_groupt: 214 | uint32 logstartaddr 215 | uint32 Obytes 216 | uint8 *outputs 217 | uint32 Ibytes 218 | uint8 *inputs 219 | boolean hasdc 220 | uint16 DCnext 221 | int16 Ebuscurrent 222 | uint8 blockLRW 223 | uint16 nsegments 224 | uint16 Isegment 225 | uint16 Ioffset 226 | uint16 outputsWKC 227 | uint16 inputsWKC 228 | boolean docheckstate 229 | uint32 *IOsegment #[EC_MAXIOSEGMENTS] 230 | 231 | ctypedef struct ec_idxstackT: 232 | uint8 pushed 233 | uint8 pulled 234 | uint8 *idx #[EC_MAXBUF] 235 | void **data #[EC_MAXBUF] 236 | uint16 *length #[EC_MAXBUF] 237 | 238 | ctypedef struct ec_eringt: 239 | int16 head 240 | int16 tail 241 | ec_errort *Error #[EC_MAXELIST + 1] 242 | 243 | ctypedef struct ec_SMcommtypet: 244 | uint8 n 245 | uint8 nu1 246 | uint8 *SMtype #[EC_MAXSM] 247 | 248 | ctypedef struct ec_PDOassignt: 249 | uint8 n 250 | uint8 nu1 251 | uint16 *index #[256] 252 | 253 | ctypedef struct ec_PDOdesct: 254 | uint8 n 255 | uint8 nu1 256 | uint32 *PDO #[256] 257 | 258 | ctypedef struct ec_eepromSMt: 259 | uint16 Startpos 260 | uint8 nSM 261 | uint16 PhStart 262 | uint16 Plength 263 | uint8 Creg 264 | uint8 Sreg 265 | uint8 Activate 266 | uint8 PDIctrl 267 | 268 | ctypedef struct ec_eepromFMMUt: 269 | uint16 Startpos 270 | uint8 nFMMU 271 | uint8 FMMU0 272 | uint8 FMMU1 273 | uint8 FMMU2 274 | uint8 FMMU3 275 | 276 | ctypedef struct ecx_contextt: 277 | ecx_portt *port 278 | ec_slavet *slavelist 279 | int *slavecount 280 | int maxslave 281 | ec_groupt *grouplist 282 | int maxgroup 283 | uint8 *esibuf 284 | uint32 *esimap 285 | uint16 esislave 286 | ec_eringt *elist 287 | ec_idxstackT *idxstack 288 | boolean *ecaterror 289 | int64 *DCtime 290 | ec_SMcommtypet *SMcommtype 291 | ec_PDOassignt *PDOassign 292 | ec_PDOdesct *PDOdesc 293 | ec_eepromSMt *eepSM 294 | ec_eepromFMMUt *eepFMMU 295 | int (*FOEhook)(uint16 slave, int packetnumber, int datasize) 296 | int (*EOEhook)(ecx_contextt* context, uint16 slave, void* eoembx) 297 | int manualstatechange 298 | 299 | ctypedef struct ec_ODlistt: 300 | uint16 Slave 301 | uint16 Entries 302 | uint16 *Index #[EC_MAXODLIST] 303 | uint16 *DataType #[EC_MAXODLIST] 304 | uint8 *ObjectCode #[EC_MAXODLIST] 305 | uint8 *MaxSub #[EC_MAXODLIST] 306 | char **Name #[EC_MAXODLIST][EC_MAXNAME+1] 307 | 308 | ctypedef struct ec_OElistt: 309 | uint16 Entries 310 | uint8 *ValueInfo #[EC_MAXOELIST] 311 | uint16 *DataType #[EC_MAXOELIST] 312 | uint16 *BitLength #[EC_MAXOELIST] 313 | uint16 *ObjAccess #[EC_MAXOELIST] 314 | char **Name #[EC_MAXOELIST][EC_MAXNAME+1] 315 | 316 | int ecx_init(ecx_contextt* context, char* ifname) 317 | int ecx_init_redundant(ecx_contextt *context, ecx_redportt *redport, const char *ifname, char *if2name) 318 | void ecx_close(ecx_contextt *context) 319 | int ecx_config_map_group(ecx_contextt *context, void *pIOmap, uint8 group) 320 | int ecx_config_overlap_map_group(ecx_contextt *context, void *pIOmap, uint8 group) 321 | int ecx_readODlist(ecx_contextt *context, uint16 Slave, ec_ODlistt *pODlist) 322 | int ecx_readODdescription(ecx_contextt *context, uint16 Item, ec_ODlistt *pODlist) 323 | int ecx_readOE(ecx_contextt *context, uint16 Item, ec_ODlistt *pODlist, ec_OElistt *pOElist) 324 | 325 | int ecx_readstate(ecx_contextt *context) 326 | int ecx_writestate(ecx_contextt *context, uint16 slave) 327 | uint16 ecx_statecheck(ecx_contextt *context, uint16 slave, uint16 reqstate, int timeout) 328 | 329 | int ecx_send_overlap_processdata(ecx_contextt *context) 330 | 331 | int ecx_recover_slave(ecx_contextt *context, uint16 slave, int timeout) 332 | int ecx_reconfig_slave(ecx_contextt *context, uint16 slave, int timeout) 333 | 334 | int ecx_mbxreceive(ecx_contextt *context, uint16 slave, ec_mbxbuft *mbx, int timeout) 335 | void ec_clearmbx(ec_mbxbuft *Mbx) 336 | boolean ecx_poperror(ecx_contextt *context, ec_errort *Ec) 337 | const char* ec_sdoerror2string(uint32 sdoerrorcode) 338 | char* ec_mbxerror2string(uint16 errorcode) 339 | 340 | boolean ecx_configdc(ecx_contextt *context) 341 | void ecx_dcsync0(ecx_contextt *context, uint16 slave, boolean act, uint32 CyclTime, int32 CyclShift) 342 | void ecx_dcsync01(ecx_contextt *context, uint16 slave, boolean act, uint32 CyclTime0, uint32 CyclTime1, int32 CyclShift) 343 | 344 | char* ec_ALstatuscode2string(uint16 ALstatuscode) 345 | 346 | uint32 ecx_readeeprom(ecx_contextt *context, uint16 slave, uint16 eeproma, int timeout) 347 | int ecx_writeeeprom(ecx_contextt *context, uint16 slave, uint16 eeproma, uint16 data, int timeout) 348 | 349 | int ecx_FPWR(ecx_portt *port, uint16 ADP, uint16 ADO, uint16 length, void *data, int timeout) 350 | int ecx_FPRD(ecx_portt *port, uint16 ADP, uint16 ADO, uint16 length, void *data, int timeout) 351 | 352 | cdef extern from "ethercat.h" nogil: 353 | int ecx_config_init(ecx_contextt *context, uint8 usetable) 354 | int ecx_send_processdata(ecx_contextt *context) 355 | int ecx_receive_processdata(ecx_contextt *context, int timeout) 356 | int ecx_FOEwrite(ecx_contextt *context, uint16 slave, char *filename, uint32 password, int psize, void *p, int timeout) 357 | int ecx_FOEread(ecx_contextt *context, uint16 slave, char *filename, uint32 password, int *psize, void *p, int timeout) 358 | int ecx_SDOread(ecx_contextt *context, uint16 slave, uint16 index, uint8 subindex, boolean CA, int *psize, void *p, int timeout) 359 | int ecx_SDOwrite(ecx_contextt *context, uint16 slave, uint16 index, uint8 subindex, boolean CA, int psize, void *p, int Timeout) -------------------------------------------------------------------------------- /src/pysoem/pysoem.pyx: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Benjamin Partzsch 2 | # 3 | # This file is part of the PySOEM project and licenced under the MIT license. 4 | # Check the license terms in the LICENSE file. 5 | # 6 | # PySOEM is a Cython wrapper for the Simple Open EtherCAT Master (SOEM) library 7 | # (https://github.com/OpenEtherCATsociety/SOEM). 8 | # 9 | # EtherCAT is a registered trademark of Beckhoff Automation GmbH. 10 | # 11 | # 12 | """PySOEM is a Cython wrapper for the SOEM library.""" 13 | 14 | # 15 | # This will result in the creation of the `pysoem.pysoem` module. 16 | # 17 | 18 | cimport cpysoem 19 | 20 | import sys 21 | import logging 22 | import collections 23 | import time 24 | import contextlib 25 | import warnings 26 | 27 | from cpython.mem cimport PyMem_Malloc, PyMem_Realloc, PyMem_Free 28 | from cpython.bytes cimport PyBytes_FromString, PyBytes_FromStringAndSize 29 | from libc.stdint cimport int8_t, int16_t, int32_t, int64_t, uint8_t, uint16_t, uint32_t, uint64_t 30 | from libc.string cimport memcpy, memset 31 | from cpython.ref cimport Py_INCREF, Py_DECREF 32 | 33 | logger = logging.getLogger(__name__) 34 | 35 | NONE_STATE = cpysoem.EC_STATE_NONE 36 | INIT_STATE = cpysoem.EC_STATE_INIT 37 | PREOP_STATE = cpysoem.EC_STATE_PRE_OP 38 | BOOT_STATE = cpysoem.EC_STATE_BOOT 39 | SAFEOP_STATE = cpysoem.EC_STATE_SAFE_OP 40 | OP_STATE = cpysoem.EC_STATE_OPERATIONAL 41 | STATE_ACK = cpysoem.EC_STATE_ACK 42 | STATE_ERROR = cpysoem.EC_STATE_ERROR 43 | 44 | ECT_REG_WD_DIV = 0x0400 45 | ECT_REG_WD_TIME_PDI = 0x0410 46 | ECT_REG_WD_TIME_PROCESSDATA = 0x0420 47 | ECT_REG_SM0 = 0x0800 48 | ECT_REG_SM1 = ECT_REG_SM0 + 0x08 49 | 50 | ECT_COEDET_SDO = 0x01 51 | ECT_COEDET_SDOINFO = 0x02 52 | ECT_COEDET_PDOASSIGN = 0x04 53 | ECT_COEDET_PDOCONFIG = 0x08 54 | ECT_COEDET_UPLOAD = 0x10 55 | ECT_COEDET_SDOCA = 0x20 56 | 57 | cdef class CdefTimeouts: 58 | 59 | cdef cpysoem.Ttimeouts* _t 60 | 61 | def __cinit__(self): 62 | self._t = &cpysoem.soem_timeouts 63 | 64 | @property 65 | def ret(self) -> int: 66 | return self._t.ret 67 | 68 | @ret.setter 69 | def ret(self, value: int): 70 | self._t.ret = value 71 | 72 | @property 73 | def safe(self) -> int: 74 | return self._t.safe 75 | 76 | @safe.setter 77 | def safe(self, value: int): 78 | self._t.safe = value 79 | 80 | @property 81 | def eeprom(self) -> int: 82 | return self._t.eeprom 83 | 84 | @eeprom.setter 85 | def eeprom(self, value: int): 86 | self._t.eeprom = value 87 | 88 | @property 89 | def tx_mailbox(self) -> int: 90 | return self._t.tx_mailbox 91 | 92 | @tx_mailbox.setter 93 | def tx_mailbox(self, value: int): 94 | self._t.tx_mailbox = value 95 | 96 | @property 97 | def rx_mailbox(self) -> int: 98 | return self._t.rx_mailbox 99 | 100 | @rx_mailbox.setter 101 | def rx_mailbox(self, value: int): 102 | self._t.rx_mailbox = value 103 | 104 | @property 105 | def state(self) -> int: 106 | return self._t.state 107 | 108 | @state.setter 109 | def state(self, value: int): 110 | self._t.state = value 111 | 112 | cdef class CdefSettings: 113 | 114 | cdef public CdefTimeouts timeouts 115 | cdef public cpysoem.boolean always_release_gil 116 | 117 | def __init__(self): 118 | self.timeouts = CdefTimeouts() 119 | self.always_release_gil = False 120 | 121 | settings = CdefSettings() 122 | 123 | cpdef enum ec_datatype: 124 | ECT_BOOLEAN = 0x0001, 125 | ECT_INTEGER8 = 0x0002, 126 | ECT_INTEGER16 = 0x0003, 127 | ECT_INTEGER32 = 0x0004, 128 | ECT_UNSIGNED8 = 0x0005, 129 | ECT_UNSIGNED16 = 0x0006, 130 | ECT_UNSIGNED32 = 0x0007, 131 | ECT_REAL32 = 0x0008, 132 | ECT_VISIBLE_STRING = 0x0009, 133 | ECT_OCTET_STRING = 0x000A, 134 | ECT_UNICODE_STRING = 0x000B, 135 | ECT_TIME_OF_DAY = 0x000C, 136 | ECT_TIME_DIFFERENCE = 0x000D, 137 | ECT_DOMAIN = 0x000F, 138 | ECT_INTEGER24 = 0x0010, 139 | ECT_REAL64 = 0x0011, 140 | ECT_INTEGER64 = 0x0015, 141 | ECT_UNSIGNED24 = 0x0016, 142 | ECT_UNSIGNED64 = 0x001B, 143 | ECT_BIT1 = 0x0030, 144 | ECT_BIT2 = 0x0031, 145 | ECT_BIT3 = 0x0032, 146 | ECT_BIT4 = 0x0033, 147 | ECT_BIT5 = 0x0034, 148 | ECT_BIT6 = 0x0035, 149 | ECT_BIT7 = 0x0036, 150 | ECT_BIT8 = 0x0037 151 | 152 | cdef struct CdefMasterSettings: 153 | int* sdo_read_timeout 154 | int* sdo_write_timeout 155 | 156 | def find_adapters(): 157 | """Create a list of available network adapters. 158 | 159 | Returns: 160 | list[Adapter]: Each element of the list has a name an desc attribute. 161 | 162 | """ 163 | cdef cpysoem.ec_adaptert* _ec_adapter = cpysoem.ec_find_adapters() 164 | Adapter = collections.namedtuple('Adapter', ['name', 'desc']) 165 | adapters = [] 166 | while not _ec_adapter == NULL: 167 | adapters.append(Adapter(_ec_adapter.name.decode('utf8'), _ec_adapter.desc)) 168 | _ec_adapter = _ec_adapter.next 169 | return adapters 170 | 171 | 172 | @contextlib.contextmanager 173 | def open(ifname): 174 | """Context manager function to create a Master object. 175 | 176 | .. versionadded:: 1.1.0 177 | """ 178 | master = Master() 179 | master.open(ifname) 180 | yield master 181 | master.close() 182 | 183 | 184 | def al_status_code_to_string(code): 185 | """Look up text string that belongs to AL status code. 186 | 187 | Args: 188 | arg1 (uint16): AL status code as defined in EtherCAT protocol. 189 | 190 | Returns: 191 | str: A verbal description of status code 192 | 193 | """ 194 | return cpysoem.ec_ALstatuscode2string(code).decode('utf8'); 195 | 196 | 197 | class Master(CdefMaster): 198 | """Representing a logical EtherCAT master device. 199 | 200 | For each network interface you can have a Master instance. 201 | 202 | Attributes: 203 | slaves: Gets a list of the slaves found during config_init. The slave instances are of type :class:`CdefSlave`. 204 | sdo_read_timeout: timeout for SDO read access for all slaves connected 205 | sdo_write_timeout: timeout for SDO write access for all slaves connected 206 | always_release_gil : true to always release the GIL 207 | """ 208 | pass 209 | 210 | 211 | cdef enum: 212 | EC_MAXSLAVE = 200 213 | EC_MAXGROUP = 1 214 | EC_MAXEEPBITMAP = 128 215 | EC_MAXEEPBUF = EC_MAXEEPBITMAP * 32 216 | EC_MAXMAPT = 8 217 | EC_IOMAPSIZE = 4096 218 | 219 | cdef class CdefMaster: 220 | """Representing a logical EtherCAT master device. 221 | 222 | Please do not use this class directly, but the class Master instead. 223 | Master is a typical Python object, with all it's benefits over 224 | cdef classes. For example you can add new attributes dynamically. 225 | """ 226 | 227 | cdef cpysoem.ec_slavet _ec_slave[EC_MAXSLAVE] 228 | cdef int _ec_slavecount 229 | cdef cpysoem.ec_groupt _ec_group[EC_MAXGROUP] 230 | cdef cpysoem.uint8 _ec_esibuf[EC_MAXEEPBUF] 231 | cdef cpysoem.uint32 _ec_esimap[EC_MAXEEPBITMAP] 232 | cdef cpysoem.ec_eringt _ec_elist 233 | cdef cpysoem.ec_idxstackT _ec_idxstack 234 | cdef cpysoem.ec_SMcommtypet _ec_SMcommtype[EC_MAXMAPT] 235 | cdef cpysoem.ec_PDOassignt _ec_PDOassign[EC_MAXMAPT] 236 | cdef cpysoem.ec_PDOdesct _ec_PDOdesc[EC_MAXMAPT] 237 | cdef cpysoem.ec_eepromSMt _ec_SM 238 | cdef cpysoem.ec_eepromFMMUt _ec_FMMU 239 | cdef cpysoem.boolean _EcatError 240 | cdef cpysoem.int64 _ec_DCtime 241 | cdef cpysoem.ecx_portt _ecx_port 242 | cdef cpysoem.ecx_redportt _ecx_redport 243 | 244 | cdef cpysoem.ecx_contextt _ecx_contextt 245 | cdef char io_map[EC_IOMAPSIZE] 246 | cdef CdefMasterSettings _settings 247 | cdef public int sdo_read_timeout 248 | cdef public int sdo_write_timeout 249 | cdef public cpysoem.boolean always_release_gil 250 | cdef readonly cpysoem.boolean context_initialized 251 | 252 | state = property(_get_state, _set_state) 253 | expected_wkc = property(_get_expected_wkc) 254 | dc_time = property(_get_dc_time) 255 | manual_state_change = property(_get_manual_state_change, _set_manual_state_change) 256 | 257 | def __cinit__(self): 258 | self._ecx_contextt.port = &self._ecx_port 259 | self._ecx_contextt.slavelist = &self._ec_slave[0] 260 | self._ecx_contextt.slavecount = &self._ec_slavecount 261 | self._ecx_contextt.maxslave = EC_MAXSLAVE 262 | self._ecx_contextt.grouplist = &self._ec_group[0] 263 | self._ecx_contextt.maxgroup = EC_MAXGROUP 264 | self._ecx_contextt.esibuf = &self._ec_esibuf[0] 265 | self._ecx_contextt.esimap = &self._ec_esimap[0] 266 | self._ecx_contextt.esislave = 0 267 | self._ecx_contextt.elist = &self._ec_elist 268 | self._ecx_contextt.idxstack = &self._ec_idxstack 269 | self._EcatError = 0 270 | self._ecx_contextt.ecaterror = &self._EcatError 271 | self._ecx_contextt.DCtime = &self._ec_DCtime 272 | self._ecx_contextt.SMcommtype = &self._ec_SMcommtype[0] 273 | self._ecx_contextt.PDOassign = &self._ec_PDOassign[0] 274 | self._ecx_contextt.PDOdesc = &self._ec_PDOdesc[0] 275 | self._ecx_contextt.eepSM = &self._ec_SM 276 | self._ecx_contextt.eepFMMU = &self._ec_FMMU 277 | self._ecx_contextt.FOEhook = NULL 278 | self._ecx_contextt.manualstatechange = 0 279 | 280 | self.slaves = None 281 | self.sdo_read_timeout = 700000 282 | self.sdo_write_timeout = 700000 283 | self.always_release_gil = settings.always_release_gil 284 | self._settings.sdo_read_timeout = &self.sdo_read_timeout 285 | self._settings.sdo_write_timeout = &self.sdo_write_timeout 286 | self.context_initialized = False 287 | 288 | def open(self, ifname, ifname_red=None): 289 | """Initialize and open network interface. 290 | 291 | On Linux the name of the interface is the same as usd by the system, e.g. ``eth0``, and as displayed by 292 | ``ip addr``. 293 | 294 | On Windows the names of the interfaces look like ``\\Device\\NPF_{1D123456-1E12-1C12-12F1-1234E123453B}``. 295 | Finding the kind of name that SOEM expects is not straightforward. The most practical way is to use the 296 | :func:`~find_adapters` method to find your available interfaces. 297 | 298 | Args: 299 | ifname(str): Interface name. 300 | ifname_red(:obj:`str`, optional): Interface name of the second network interface card for redundancy. 301 | Put to None if not used. 302 | 303 | Raises: 304 | ConnectionError: When the specified interface dose not exist or 305 | you have no permission to open the interface 306 | """ 307 | if ifname_red is None: 308 | ret_val = cpysoem.ecx_init(&self._ecx_contextt, ifname.encode('utf8')) 309 | else: 310 | ret_val = cpysoem.ecx_init_redundant(&self._ecx_contextt, &self._ecx_redport, ifname.encode('utf8'), ifname_red.encode('utf8')) 311 | if ret_val == 0: 312 | raise ConnectionError('could not open interface {}'.format(ifname)) 313 | 314 | self.context_initialized = True 315 | 316 | def check_context_is_initialized(self): 317 | if not self.context_initialized: 318 | raise NetworkInterfaceNotOpenError("SOEM Network interface is not initialized or has been closed. Call Master.open() first") 319 | 320 | cdef int __config_init_nogil(self, uint8_t usetable): 321 | """Enumerate and init all slaves without GIL. 322 | 323 | Args: 324 | usetable (uint8_t): True when using configtable to init slaves, False otherwise. 325 | """ 326 | cdef int ret_val 327 | 328 | Py_INCREF(self) 329 | with nogil: 330 | ret_val = cpysoem.ecx_config_init(&self._ecx_contextt, usetable) 331 | Py_DECREF(self) 332 | 333 | return ret_val 334 | 335 | cpdef cpysoem.boolean check_release_gil(self, release_gil): 336 | """Checks if the GIL should be released. 337 | 338 | Args: 339 | release_gil (boolean): True if the GIL should be released, False otherwise. 340 | """ 341 | if release_gil is not None: 342 | return release_gil 343 | return self.always_release_gil 344 | 345 | def config_init(self, usetable=False, *, release_gil=None): 346 | """Enumerate and init all slaves. 347 | 348 | Args: 349 | usetable (bool): True when using configtable to init slaves, False otherwise. 350 | release_gil (:obj:`bool`, optional): True to initialize the slaves releasing the GIL. Defaults to False. 351 | 352 | Returns: 353 | int: Working counter of slave discover datagram = number of slaves found, -1 when no slave is connected 354 | """ 355 | release_gil = self.check_release_gil(release_gil) 356 | self.check_context_is_initialized() 357 | self.slaves = [] 358 | 359 | cdef int ret_val 360 | if release_gil: 361 | ret_val = self.__config_init_nogil(usetable) 362 | else: 363 | ret_val = cpysoem.ecx_config_init(&self._ecx_contextt, usetable) 364 | 365 | if ret_val > 0: 366 | for i in range(self._ec_slavecount): 367 | self.slaves.append(self._get_slave(i)) 368 | return ret_val 369 | 370 | def config_map(self): 371 | """Map all slaves PDOs in IO map. 372 | 373 | Returns: 374 | int: IO map size (sum of all PDO in an out data) 375 | """ 376 | self.check_context_is_initialized() 377 | cdef _CallbackData cd 378 | # ecx_config_map_group returns the actual IO map size (not an error value), expect the value to be less than EC_IOMAPSIZE 379 | ret_val = cpysoem.ecx_config_map_group(&self._ecx_contextt, &self.io_map, 0) 380 | # check for exceptions raised in the config functions 381 | for slave in self.slaves: 382 | cd = slave._cd 383 | if cd.exc_raised: 384 | raise cd.exc_info[0], cd.exc_info[1], cd.exc_info[2] 385 | logger.debug('io map size: {}'.format(ret_val)) 386 | # raise an exception if one or more mailbox errors occured within ecx_config_map_group call 387 | error_list = self._collect_mailbox_errors() 388 | if len(error_list) > 0: 389 | raise ConfigMapError(error_list) 390 | return ret_val 391 | 392 | def config_overlap_map(self): 393 | """Map all slaves PDOs to overlapping IO map. 394 | 395 | Returns: 396 | int: IO map size (sum of all PDO in an out data) 397 | """ 398 | self.check_context_is_initialized() 399 | cdef _CallbackData cd 400 | # ecx_config_map_group returns the actual IO map size (not an error value), expect the value to be less than EC_IOMAPSIZE 401 | ret_val = cpysoem.ecx_config_overlap_map_group(&self._ecx_contextt, &self.io_map, 0) 402 | # check for exceptions raised in the config functions 403 | for slave in self.slaves: 404 | cd = slave._cd 405 | if cd.exc_raised: 406 | raise cd.exc_info[0],cd.exc_info[1],cd.exc_info[2] 407 | logger.debug('io map size: {}'.format(ret_val)) 408 | # raise an exception if one or more mailbox errors occured within ecx_config_overlap_map_group call 409 | error_list = self._collect_mailbox_errors() 410 | if len(error_list) > 0: 411 | raise ConfigMapError(error_list) 412 | 413 | return ret_val 414 | 415 | def _collect_mailbox_errors(self): 416 | # collect SDO or mailbox errors that occurred during PDO configuration read in ecx_config_map_group 417 | error_list = [] 418 | cdef cpysoem.ec_errort err 419 | while cpysoem.ecx_poperror(&self._ecx_contextt, &err): 420 | if err.Etype == cpysoem.EC_ERR_TYPE_SDO_ERROR: 421 | error_list.append(SdoError(err.Slave, 422 | err.Index, 423 | err.SubIdx, 424 | err.AbortCode, cpysoem.ec_sdoerror2string(err.AbortCode).decode('utf8'))) 425 | elif err.Etype == cpysoem.EC_ERR_TYPE_MBX_ERROR: 426 | error_list.append(MailboxError(err.Slave, 427 | err.ErrorCode, 428 | cpysoem.ec_mbxerror2string(err.ErrorCode).decode('utf8'))) 429 | elif err.Etype == cpysoem.EC_ERR_TYPE_PACKET_ERROR: 430 | error_list.append(PacketError(err.Slave, 431 | err.ErrorCode)) 432 | else: 433 | error_list.append(Exception('unexpected error')) 434 | return error_list 435 | 436 | def config_dc(self): 437 | """Locate DC slaves, measure propagation delays. 438 | 439 | Returns: 440 | bool: if slaves are found with DC 441 | """ 442 | self.check_context_is_initialized() 443 | return cpysoem.ecx_configdc(&self._ecx_contextt) 444 | 445 | def close(self): 446 | """Close the network interface. 447 | 448 | """ 449 | # ecx_close returns nothing 450 | self.context_initialized = False 451 | cpysoem.ecx_close(&self._ecx_contextt) 452 | 453 | def read_state(self): 454 | """Read all slaves states. 455 | 456 | Returns: 457 | int: lowest state found 458 | """ 459 | self.check_context_is_initialized() 460 | return cpysoem.ecx_readstate(&self._ecx_contextt) 461 | 462 | def write_state(self): 463 | """Write all slaves state. 464 | 465 | The function does not check if the actual state is changed. 466 | 467 | Returns: 468 | int: Working counter or EC_NOFRAME 469 | """ 470 | self.check_context_is_initialized() 471 | return cpysoem.ecx_writestate(&self._ecx_contextt, 0) 472 | 473 | def state_check(self, int expected_state, timeout=50000): 474 | """Check actual slave state. 475 | 476 | This is a blocking function. 477 | To refresh the state of all slaves read_state() should be called 478 | 479 | Args: 480 | expected_state (int): Requested state 481 | timeout (int): Timeout value in us 482 | 483 | Returns: 484 | int: Requested state, or found state after timeout 485 | """ 486 | self.check_context_is_initialized() 487 | return cpysoem.ecx_statecheck(&self._ecx_contextt, 0, expected_state, timeout) 488 | 489 | cdef int __send_processdata_nogil(self): 490 | """Transmit processdata to slaves without GIL.""" 491 | cdef int result 492 | 493 | Py_INCREF(self) 494 | with nogil: 495 | result = cpysoem.ecx_send_processdata(&self._ecx_contextt) 496 | Py_DECREF(self) 497 | 498 | return result 499 | 500 | def send_processdata(self, *, release_gil=None): 501 | """Transmit processdata to slaves. 502 | 503 | Uses LRW, or LRD/LWR if LRW is not allowed (blockLRW). 504 | Both the input and output processdata are transmitted. 505 | The outputs with the actual data, the inputs have a placeholder. 506 | The inputs are gathered with the receive processdata function. 507 | In contrast to the base LRW function this function is non-blocking. 508 | If the processdata does not fit in one datagram, multiple are used. 509 | In order to recombine the slave response, a stack is used. 510 | 511 | Args: 512 | release_gil (:obj:`bool`, optional): True to transmit processdata releasing the GIL. Defaults to False. 513 | 514 | Returns: 515 | int: >0 if processdata is transmitted, might only by 0 if config map is not configured properly 516 | """ 517 | release_gil = self.check_release_gil(release_gil) 518 | self.check_context_is_initialized() 519 | if release_gil: 520 | return self.__send_processdata_nogil() 521 | return cpysoem.ecx_send_processdata(&self._ecx_contextt) 522 | 523 | def send_overlap_processdata(self): 524 | """Transmit overlap processdata to slaves. 525 | 526 | Returns: 527 | int: >0 if processdata is transmitted, might only by 0 if config map is not configured properly 528 | """ 529 | self.check_context_is_initialized() 530 | return cpysoem.ecx_send_overlap_processdata(&self._ecx_contextt) 531 | 532 | cdef int __receive_processdata_nogil(self, int timeout): 533 | """Receive processdata from slaves without GIL. 534 | 535 | Args: 536 | timeout (int): Timeout in us. 537 | """ 538 | cdef int result 539 | 540 | Py_INCREF(self) 541 | with nogil: 542 | result = cpysoem.ecx_receive_processdata(&self._ecx_contextt, timeout) 543 | Py_DECREF(self) 544 | 545 | return result 546 | 547 | def receive_processdata(self, timeout=2000, *, release_gil=None): 548 | """Receive processdata from slaves. 549 | 550 | Second part from send_processdata(). 551 | Received datagrams are recombined with the processdata with help from the stack. 552 | If a datagram contains input processdata it copies it to the processdata structure. 553 | 554 | Args: 555 | timeout (int): Timeout in us. 556 | release_gil (:obj:`bool`, optional): True to receive processdata releasing the GIL. Defaults to False. 557 | Returns 558 | int: Working Counter 559 | """ 560 | release_gil = self.check_release_gil(release_gil) 561 | self.check_context_is_initialized() 562 | if release_gil: 563 | return self.__receive_processdata_nogil(timeout) 564 | return cpysoem.ecx_receive_processdata(&self._ecx_contextt, timeout) 565 | 566 | def _get_slave(self, int pos): 567 | if pos < 0: 568 | raise IndexError('requested slave device is not available') 569 | if pos >= self._ec_slavecount: 570 | raise IndexError('requested slave device is not available') 571 | ethercat_slave = CdefSlave(pos+1) 572 | ethercat_slave._master = self 573 | ethercat_slave._ecx_contextt = &self._ecx_contextt 574 | ethercat_slave._ec_slave = &self._ec_slave[pos+1] # +1 as _ec_slave[0] is reserved 575 | ethercat_slave._the_masters_settings = &self._settings 576 | return ethercat_slave 577 | 578 | def _get_state(self): 579 | """Can be used to check if all slaves are in Operational state, or to request a new state for all slaves. 580 | 581 | Make sure to call write_state(), once a new state for all slaves was set. 582 | """ 583 | return self._ec_slave[0].state 584 | 585 | def _set_state(self, value): 586 | self._ec_slave[0].state = value 587 | 588 | def _get_expected_wkc(self): 589 | """Calculates the expected Working Counter""" 590 | return (self._ec_group[0].outputsWKC * 2) + self._ec_group[0].inputsWKC 591 | 592 | def _get_dc_time(self): 593 | """DC time in ns required to synchronize the EtherCAT cycle with SYNC0 cycles. 594 | 595 | Note EtherCAT cycle here means the call of send_processdata and receive_processdata.""" 596 | return self._ec_DCtime 597 | 598 | def _set_manual_state_change(self, int manual_state_change): 599 | """Set manualstatechange variable in context. 600 | 601 | Flag to control legacy automatic state change or manual state change in functions 602 | config_init() and config_map() 603 | Flag value == 0 is legacy automatic state 604 | Flag value != 0 and states must be handled manually 605 | Args: 606 | manual_state_change (int): The manual state change flag. 607 | 608 | .. versionadded:: 1.0.5 609 | """ 610 | self._ecx_contextt.manualstatechange = manual_state_change 611 | 612 | def _get_manual_state_change(self): 613 | return self._ecx_contextt.manualstatechange 614 | 615 | 616 | 617 | class SdoError(Exception): 618 | """Sdo read or write abort 619 | 620 | Attributes: 621 | slave_pos (int): position of the slave 622 | abort_code (int): specified sdo abort code 623 | desc (str): error description 624 | """ 625 | 626 | def __init__(self, slave_pos, index, subindex, abort_code, desc): 627 | self.slave_pos = slave_pos 628 | self.index = index 629 | self.subindex = subindex 630 | self.abort_code = abort_code 631 | self.desc = desc 632 | 633 | class Emergency(Exception): 634 | """Emergency message. 635 | 636 | Attributes: 637 | slave_pos (int): position of the slave 638 | error_code (int): error code 639 | error_reg (int): error register 640 | b1 (int): data byte [0] 641 | w1 (int): data bytes [1,2] 642 | w2 (int): data bytes [3,4] 643 | """ 644 | 645 | def __init__(self, slave_pos, error_code, error_reg, b1, w1, w2): 646 | self.slave_pos = slave_pos 647 | self.error_code = error_code 648 | self.error_reg = error_reg 649 | self.b1 = b1 650 | self.w1 = w1 651 | self.w2 = w2 652 | 653 | def __str__(self): 654 | b1w1w2_bytes = bytes([self.b1]) + self.w1.to_bytes(length=2, byteorder='little') + self.w2.to_bytes(length=2, byteorder='little') 655 | b1w1w2_str = ','.join(format(x, '02x') for x in b1w1w2_bytes) 656 | return f'Slave {self.slave_pos}: {self.error_code:04x}, {self.error_reg:02x}, ({b1w1w2_str})' 657 | 658 | 659 | class SdoInfoError(Exception): 660 | """Errors during Object directory info read 661 | 662 | Attributes: 663 | message (str): error message 664 | """ 665 | 666 | def __init__(self, message): 667 | self.message = message 668 | 669 | 670 | class MailboxError(Exception): 671 | """Errors in mailbox communication 672 | 673 | Attributes: 674 | slave_pos (int): position of the slave 675 | error_code (int): error code 676 | desc (str): error description 677 | """ 678 | 679 | def __init__(self, slave_pos, error_code, desc): 680 | self.slave_pos = slave_pos 681 | self.error_code = error_code 682 | self.desc = desc 683 | 684 | 685 | class PacketError(Exception): 686 | """Errors related to mailbox communication 687 | 688 | Attributes: 689 | slave_pos (int): position of the slave 690 | error_code (int): error code 691 | message (str): error message 692 | desc (str): error description 693 | """ 694 | 695 | # based on the comments in the soem code 696 | _code_desc = { 697 | 1: 'Unexpected frame returned', 698 | 3: 'Data container too small for type', 699 | } 700 | 701 | def __init__(self, slave_pos, error_code): 702 | self.slave_pos = slave_pos 703 | self.error_code = error_code 704 | 705 | def _get_desc(self): 706 | return self._code_desc[self.error_code] 707 | 708 | desc = property(_get_desc) 709 | 710 | 711 | class ConfigMapError(Exception): 712 | """Errors during Object directory info read 713 | 714 | Attributes: 715 | error_list (str): a list of exceptions of type MailboxError or SdoError 716 | """ 717 | 718 | def __init__(self, error_list): 719 | self.error_list = error_list 720 | 721 | 722 | class EepromError(Exception): 723 | """EEPROM access error 724 | 725 | Attributes: 726 | message (str): error message 727 | """ 728 | 729 | def __init__(self, message): 730 | self.message = message 731 | 732 | 733 | class WkcError(Exception): 734 | """Working counter error. 735 | 736 | Attributes: 737 | message (str): error message 738 | wkc (int): Working counter 739 | """ 740 | 741 | def __init__(self, message=None, wkc=None): 742 | self.message = message 743 | self.wkc = wkc 744 | 745 | class NetworkInterfaceNotOpenError(Exception): 746 | """Error when a master or slave method is used and the context has not been initialized.""" 747 | pass 748 | 749 | 750 | cdef class _CallbackData: 751 | cdef: 752 | object slave 753 | object func 754 | object exc_raised 755 | object exc_info 756 | 757 | 758 | class SiiOffset: 759 | """Item offsets in SII general section.""" 760 | # Took it from ethercattype.h but no type was given. 761 | MAN = 0x0008 762 | ID = 0x000A 763 | REV = 0x000B 764 | BOOT_RX_MBX = 0x0014 765 | BOOT_TX_MBX = 0x0016 766 | STD_RX_MBX = 0x0018 767 | STD_TX_MBX = 0x001A 768 | MBX_PROTO = 0x001C 769 | 770 | 771 | cdef enum: 772 | EC_TIMEOUTRXM = 700000 773 | STATIC_SDO_READ_BUFFER_SIZE = 256 774 | 775 | 776 | cdef class CdefSlave: 777 | """Represents a slave device 778 | 779 | Do not use this class in application code. Instances are created 780 | by a Master instance on a successful config_init(). They then can be 781 | obtained by slaves list 782 | """ 783 | cdef readonly CdefMaster _master 784 | cdef cpysoem.ecx_contextt* _ecx_contextt 785 | cdef cpysoem.ec_slavet* _ec_slave 786 | cdef CdefMasterSettings* _the_masters_settings 787 | cdef int _pos # keep in mind that first slave has pos 1 788 | cdef public _CallbackData _cd 789 | cdef cpysoem.ec_ODlistt _ex_odlist 790 | cdef public _emcy_callbacks 791 | 792 | name = property(_get_name) 793 | man = property(_get_eep_man) 794 | id = property(_get_eep_id) 795 | rev = property(_get_eep_rev) 796 | config_func = property(_get_PO2SOconfig, _set_PO2SOconfig) 797 | setup_func = property(_get_PO2SOconfigEx, _set_PO2SOconfigEx) 798 | state = property(_get_state, _set_state) 799 | input = property(_get_input) 800 | output = property(_get_output, _set_output) 801 | al_status = property(_get_al_status) 802 | is_lost = property(_get_is_lost, _set_is_lost) 803 | od = property(_get_od) 804 | 805 | def __init__(self, pos): 806 | self._pos = pos 807 | self._cd = _CallbackData() 808 | self._cd.slave = self 809 | self._emcy_callbacks = [] 810 | 811 | def dc_sync(self, act, sync0_cycle_time, sync0_shift_time=0, sync1_cycle_time=None): 812 | """Activate or deactivate SYNC pulses at the slave. 813 | 814 | Args: 815 | act (bool): True = active, False = deactivate 816 | sync0_cycle_time (int): Cycltime SYNC0 in ns 817 | sync0_shift_time (int): Optional SYNC0 shift time in ns 818 | sync1_cycle_time (int): Optional cycltime for SYNC1 in ns. This time is a delta time in relation to SYNC0. 819 | If CylcTime1 = 0 then SYNC1 fires at the same time as SYNC0. 820 | """ 821 | self._master.check_context_is_initialized() 822 | 823 | if sync1_cycle_time is None: 824 | cpysoem.ecx_dcsync0(self._ecx_contextt, self._pos, act, sync0_cycle_time, sync0_shift_time) 825 | else: 826 | cpysoem.ecx_dcsync01(self._ecx_contextt, self._pos, act, sync0_cycle_time, sync1_cycle_time, sync0_shift_time) 827 | 828 | cdef int __sdo_read_nogil(self, uint16_t index, uint8_t subindex, int8_t ca, int size_inout, unsigned char* pbuf): 829 | """Read a CoE object without GIL. 830 | 831 | Args: 832 | index (int): Index of the object. 833 | subindex (int): Subindex of the object. 834 | ca (:obj:`bool`): complete access. 835 | size_inout (int): size in bytes of parameter buffer. 836 | pbuf (unsigned char*): pointer to parameter buffer. 837 | """ 838 | cdef int result 839 | 840 | Py_INCREF(self) 841 | with nogil: 842 | result = cpysoem.ecx_SDOread(self._ecx_contextt, self._pos, index, subindex, ca, &size_inout, pbuf, self._the_masters_settings.sdo_read_timeout[0]) 843 | Py_DECREF(self) 844 | 845 | return result 846 | 847 | def sdo_read(self, index, uint8_t subindex, int size=0, ca=False, *, release_gil=None): 848 | """Read a CoE object. 849 | 850 | When leaving out the size parameter, objects up to 256 bytes can be read. 851 | If the size of the object is expected to be bigger, increase the size parameter. 852 | 853 | Args: 854 | index (int): Index of the object. 855 | subindex (int): Subindex of the object. 856 | size (:obj:`int`, optional): The size of the reading buffer. 857 | ca (:obj:`bool`, optional): complete access. 858 | release_gil (:obj:`bool`, optional): True to read a CoE object releasing the GIL. Defaults to False. 859 | 860 | Returns: 861 | bytes: The content of the sdo object. 862 | 863 | Raises: 864 | SdoError: if write fails, the exception includes the SDO abort code 865 | MailboxError: on errors in the mailbox protocol 866 | PacketError: on packet level error 867 | WkcError: if working counter is not higher than 0, the exception includes the working counter 868 | """ 869 | release_gil = self._master.check_release_gil(release_gil=release_gil) 870 | if self._ecx_contextt == NULL: 871 | raise UnboundLocalError() 872 | 873 | self._master.check_context_is_initialized() 874 | 875 | cdef unsigned char* pbuf 876 | cdef uint8_t std_buffer[STATIC_SDO_READ_BUFFER_SIZE] 877 | cdef int size_inout 878 | if size == 0: 879 | pbuf = std_buffer 880 | size_inout = STATIC_SDO_READ_BUFFER_SIZE 881 | else: 882 | pbuf = PyMem_Malloc((size)*sizeof(unsigned char)) 883 | size_inout = size 884 | 885 | if pbuf == NULL: 886 | raise MemoryError() 887 | 888 | cdef int result 889 | if release_gil: 890 | result = self.__sdo_read_nogil(index, subindex, ca, size_inout, pbuf) 891 | else: 892 | result = cpysoem.ecx_SDOread(self._ecx_contextt, self._pos, index, subindex, ca, 893 | &size_inout, pbuf, self._the_masters_settings.sdo_read_timeout[0]) 894 | 895 | cdef cpysoem.ec_errort err 896 | while cpysoem.ecx_poperror(self._ecx_contextt, &err): 897 | assert err.Slave == self._pos 898 | 899 | if (err.Etype == cpysoem.EC_ERR_TYPE_EMERGENCY) and (len(self._emcy_callbacks) > 0): 900 | self._on_emergency(&err) 901 | else: 902 | if pbuf != std_buffer: 903 | PyMem_Free(pbuf) 904 | self._raise_exception(&err) 905 | 906 | if not result > 0: 907 | if pbuf != std_buffer: 908 | PyMem_Free(pbuf) 909 | raise WkcError(wkc=result) 910 | 911 | try: 912 | return PyBytes_FromStringAndSize(pbuf, size_inout) 913 | finally: 914 | if pbuf != std_buffer: 915 | PyMem_Free(pbuf) 916 | 917 | cdef int __sdo_write_nogil(self, uint16_t index, uint8_t subindex, int8_t ca, int size, bytes data): 918 | """Write to a CoE object without GIL. 919 | 920 | Args: 921 | index (int): Index of the object. 922 | subindex (int): Subindex of the object. 923 | ca (:obj:`bool`): complete access. 924 | size (int): size of the data to be written. 925 | data (bytes): data to be written to the object. 926 | """ 927 | cdef int result 928 | cdef unsigned char* c_data = data 929 | 930 | Py_INCREF(self) 931 | with nogil: 932 | result = cpysoem.ecx_SDOwrite(self._ecx_contextt, self._pos, index, subindex, ca, size, c_data, self._the_masters_settings.sdo_write_timeout[0]) 933 | Py_DECREF(self) 934 | 935 | return result 936 | 937 | def sdo_write(self, index, uint8_t subindex, bytes data, ca=False, *, release_gil=None): 938 | """Write to a CoE object. 939 | 940 | Args: 941 | index (int): Index of the object. 942 | subindex (int): Subindex of the object. 943 | data (bytes): data to be written to the object. 944 | ca (:obj:`bool`, optional): complete access. 945 | release_gil (:obj:`bool`, optional): True to write to a CoE object releasing the GIL. Defaults to False. 946 | 947 | Raises: 948 | SdoError: if write fails, the exception includes the SDO abort code 949 | MailboxError: on errors in the mailbox protocol 950 | PacketError: on packet level error 951 | WkcError: if working counter is not higher than 0, the exception includes the working counter 952 | """ 953 | release_gil = self._master.check_release_gil(release_gil=release_gil) 954 | self._master.check_context_is_initialized() 955 | 956 | cdef int size = len(data) 957 | cdef int result 958 | if release_gil: 959 | result = self.__sdo_write_nogil(index, subindex, ca, size, data) 960 | else: 961 | result = cpysoem.ecx_SDOwrite(self._ecx_contextt, self._pos, index, subindex, ca, 962 | size, data, self._the_masters_settings.sdo_write_timeout[0]) 963 | 964 | cdef cpysoem.ec_errort err 965 | while(cpysoem.ecx_poperror(self._ecx_contextt, &err)): 966 | if (err.Etype == cpysoem.EC_ERR_TYPE_EMERGENCY) and (len(self._emcy_callbacks) > 0): 967 | self._on_emergency(&err) 968 | else: 969 | self._raise_exception(&err) 970 | 971 | if not result > 0: 972 | raise WkcError(wkc=result) 973 | 974 | def mbx_receive(self): 975 | """Read out the slaves out mailbox - to check for emergency messages. 976 | 977 | .. versionadded:: 1.0.4 978 | 979 | :return: Work counter 980 | :rtype: int 981 | :raises Emergency: if an emergency message was received 982 | """ 983 | self._master.check_context_is_initialized() 984 | 985 | cdef cpysoem.ec_mbxbuft buf 986 | cpysoem.ec_clearmbx(&buf) 987 | cdef int wkt = cpysoem.ecx_mbxreceive(self._ecx_contextt, self._pos, &buf, 0) 988 | 989 | cdef cpysoem.ec_errort err 990 | if cpysoem.ecx_poperror(self._ecx_contextt, &err): 991 | if (err.Etype == cpysoem.EC_ERR_TYPE_EMERGENCY) and (len(self._emcy_callbacks) > 0): 992 | self._on_emergency(&err) 993 | else: 994 | self._raise_exception(&err) 995 | 996 | return wkt 997 | 998 | def write_state(self): 999 | """Write slave state. 1000 | 1001 | Note: The function does not check if the actual state is changed. 1002 | """ 1003 | self._master.check_context_is_initialized() 1004 | return cpysoem.ecx_writestate(self._ecx_contextt, self._pos) 1005 | 1006 | def state_check(self, int expected_state, timeout=2000): 1007 | """Wait for the slave to reach the state that was requested.""" 1008 | self._master.check_context_is_initialized() 1009 | return cpysoem.ecx_statecheck(self._ecx_contextt, self._pos, expected_state, timeout) 1010 | 1011 | def reconfig(self, timeout=500): 1012 | """Reconfigure slave. 1013 | 1014 | :param timeout: local timeout 1015 | :return: Slave state 1016 | :rtype: int 1017 | """ 1018 | self._master.check_context_is_initialized() 1019 | return cpysoem.ecx_reconfig_slave(self._ecx_contextt, self._pos, timeout) 1020 | 1021 | def recover(self, timeout=500): 1022 | """Recover slave. 1023 | 1024 | :param timeout: local timeout 1025 | :return: >0 if successful 1026 | :rtype: int 1027 | """ 1028 | self._master.check_context_is_initialized() 1029 | return cpysoem.ecx_recover_slave(self._ecx_contextt, self._pos, timeout) 1030 | 1031 | def eeprom_read(self, int word_address, timeout=20000): 1032 | """Read 4 byte from EEPROM 1033 | 1034 | Default timeout: 20000 us 1035 | 1036 | Args: 1037 | word_address (int): EEPROM address to read from 1038 | timeout (:obj:`int`, optional): Timeout value in us 1039 | 1040 | Returns: 1041 | bytes: EEPROM data 1042 | """ 1043 | self._master.check_context_is_initialized() 1044 | cdef uint32_t tmp = cpysoem.ecx_readeeprom(self._ecx_contextt, self._pos, word_address, timeout) 1045 | return PyBytes_FromStringAndSize(&tmp, 4) 1046 | 1047 | def eeprom_write(self, int word_address, bytes data, timeout=20000): 1048 | """Write 2 byte (1 word) to EEPROM 1049 | 1050 | Default timeout: 20000 us 1051 | 1052 | Args: 1053 | word_address (int): EEPROM address to write to 1054 | data (bytes): data (only 2 bytes are allowed) 1055 | timeout (:obj:`int`, optional): Timeout value in us 1056 | 1057 | Raises: 1058 | EepromError: if write fails 1059 | AttributeError: if data size is not 2 1060 | """ 1061 | self._master.check_context_is_initialized() 1062 | if not len(data) == 2: 1063 | raise AttributeError() 1064 | cdef uint16_t tmp 1065 | memcpy(&tmp, data, 2) 1066 | cdef int result = cpysoem.ecx_writeeeprom(self._ecx_contextt, self._pos, word_address, tmp, timeout) 1067 | if not result > 0: 1068 | raise EepromError('EEPROM write error') 1069 | 1070 | cdef int __foe_write_nogil(self, str filename, uint32_t password, int size, bytes data, int timeout): 1071 | """Write given data to device using FoE without GIL. 1072 | 1073 | Args: 1074 | filename (string): name of the target file. 1075 | password (uint32_t): password for the target file, accepted range: 0 to 2^32 - 1. 1076 | size (int): size of the file buffer. 1077 | data (bytes): data. 1078 | timeout (int): Timeout value in us. 1079 | """ 1080 | cdef int result 1081 | cdef bytes encoded_filename = filename.encode('utf-8') 1082 | cdef char* c_filename = encoded_filename 1083 | cdef unsigned char* c_data = data 1084 | 1085 | Py_INCREF(self) 1086 | with nogil: 1087 | result = cpysoem.ecx_FOEwrite(self._ecx_contextt, self._pos, c_filename, password, size, c_data, timeout) 1088 | Py_DECREF(self) 1089 | 1090 | return result 1091 | 1092 | def foe_write(self, filename, password, bytes data, timeout = 200000, *, release_gil=None): 1093 | """ Write given data to device using FoE 1094 | 1095 | Args: 1096 | filename (string): name of the target file 1097 | password (int): password for the target file, accepted range: 0 to 2^32 - 1 1098 | data (bytes): data 1099 | timeout (int): Timeout value in us 1100 | release_gil (:obj:`bool`, optional): True to FoE write releasing the GIL. Defaults to False. 1101 | """ 1102 | release_gil = self._master.check_release_gil(release_gil=release_gil) 1103 | # error handling 1104 | if self._ecx_contextt == NULL: 1105 | raise UnboundLocalError() 1106 | 1107 | self._master.check_context_is_initialized() 1108 | 1109 | cdef int result 1110 | cdef int size = len(data) 1111 | 1112 | if release_gil: 1113 | result = self.__foe_write_nogil(filename, password, size, data, timeout) 1114 | else: 1115 | result = cpysoem.ecx_FOEwrite(self._ecx_contextt, self._pos, filename.encode('utf8'), password, size, data, timeout) 1116 | 1117 | # error handling 1118 | cdef cpysoem.ec_errort err 1119 | if cpysoem.ecx_poperror(self._ecx_contextt, &err): 1120 | assert err.Slave == self._pos 1121 | self._raise_exception(&err) 1122 | 1123 | return result 1124 | 1125 | cdef int __foe_read_nogil(self, str filename, uint32_t password, int size_inout, unsigned char* pbuf, int timeout): 1126 | """Read given filename from device using FoE without GIL. 1127 | 1128 | Args: 1129 | filename (string): name of the target file. 1130 | password (int): password for target file 1131 | size_inout (int): size in bytes of file buffer. 1132 | pbuf (unsigned char*): data. 1133 | timeout (int): Timeout value in us. 1134 | """ 1135 | cdef int result 1136 | cdef bytes encoded_filename = filename.encode('utf-8') 1137 | cdef char* c_filename = encoded_filename 1138 | 1139 | Py_INCREF(self) 1140 | with nogil: 1141 | result = cpysoem.ecx_FOEread(self._ecx_contextt, self._pos, c_filename, password, &size_inout, pbuf, timeout) 1142 | Py_DECREF(self) 1143 | 1144 | return result 1145 | 1146 | def foe_read(self, filename, password, size, timeout = 200000, *, release_gil=None): 1147 | """Read given filename from device using FoE 1148 | 1149 | Args: 1150 | filename (string): name of the target file 1151 | password (int): password for target file 1152 | size (int): maximum file size 1153 | timeout (int): Timeout value in us 1154 | release_gil (:obj:`bool`, optional): True to FoE write releasing the GIL. Defaults to False. 1155 | """ 1156 | release_gil = self._master.check_release_gil(release_gil=release_gil) 1157 | if self._ecx_contextt == NULL: 1158 | raise UnboundLocalError() 1159 | 1160 | self._master.check_context_is_initialized() 1161 | 1162 | # prepare call of c function 1163 | cdef unsigned char* pbuf 1164 | cdef int size_inout 1165 | pbuf = PyMem_Malloc((size)*sizeof(unsigned char)) 1166 | size_inout = size 1167 | 1168 | cdef int result 1169 | if release_gil: 1170 | result = self.__foe_read_nogil(filename, password, size_inout, pbuf, timeout) 1171 | else: 1172 | result = cpysoem.ecx_FOEread(self._ecx_contextt, self._pos, filename.encode('utf8'), password, &size_inout, pbuf, timeout) 1173 | 1174 | # error handling 1175 | cdef cpysoem.ec_errort err 1176 | if cpysoem.ecx_poperror(self._ecx_contextt, &err): 1177 | PyMem_Free(pbuf) 1178 | assert err.Slave == self._pos 1179 | self._raise_exception(&err) 1180 | 1181 | # return data 1182 | try: 1183 | return PyBytes_FromStringAndSize(pbuf, size_inout) 1184 | finally: 1185 | PyMem_Free(pbuf) 1186 | 1187 | def amend_mbx(self, mailbox, start_address, size): 1188 | """Change the start address and size of a mailbox. 1189 | 1190 | Note that the slave must me in INIT state to do that. 1191 | 1192 | :param str mailbox: Ether 'out', or 'in' to specify which mailbox to update. 1193 | :param int start_address: New start address for the mailbox. 1194 | :param int size: New size of the mailbox. 1195 | 1196 | .. versionadded:: 1.0.6 1197 | """ 1198 | self._master.check_context_is_initialized() 1199 | 1200 | fpwr_timeout_us = 4000 1201 | if mailbox == 'out': 1202 | # Clear the slaves mailbox configuration. 1203 | self._fpwr(ECT_REG_SM0, bytes(sizeof(self._ec_slave.SM[0]))) 1204 | self._ec_slave.SM[0].StartAddr = start_address 1205 | self._ec_slave.SM[0].SMlength = size 1206 | self._ec_slave.mbx_wo = start_address 1207 | self._ec_slave.mbx_l = size 1208 | # Update the slaves mailbox configuration. 1209 | self._fpwr(ECT_REG_SM0, 1210 | PyBytes_FromStringAndSize(&self._ec_slave.SM[0], sizeof(self._ec_slave.SM[0])), 1211 | fpwr_timeout_us) 1212 | elif mailbox == 'in': 1213 | # Clear the slaves mailbox configuration. 1214 | self._fpwr(ECT_REG_SM1, bytes(sizeof(self._ec_slave.SM[1]))) 1215 | self._ec_slave.SM[1].StartAddr = start_address 1216 | self._ec_slave.SM[1].SMlength = size 1217 | self._ec_slave.mbx_ro = start_address 1218 | self._ec_slave.mbx_rl = size 1219 | # Update the slaves mailbox configuration. 1220 | self._fpwr(ECT_REG_SM1, 1221 | PyBytes_FromStringAndSize(&self._ec_slave.SM[1], sizeof(self._ec_slave.SM[1])), 1222 | fpwr_timeout_us) 1223 | else: 1224 | raise AttributeError() 1225 | 1226 | def set_watchdog(self, wd_type, wd_time_ms): 1227 | """Change the watchdog time of the PDI or Process Data watchdog. 1228 | 1229 | .. warning:: This is experimental. 1230 | 1231 | :param str wd_type: Ether 'pdi', or 'processdata' to specify the watchdog time to be updated. 1232 | :param float wd_time_ms: Watchdog time in ms. 1233 | 1234 | At the default watchdog time divider the precision is 0.1 ms. 1235 | 1236 | .. versionadded:: 1.0.6 1237 | """ 1238 | self._master.check_context_is_initialized() 1239 | 1240 | fprd_fpwr_timeout_us = 4000 1241 | wd_type_to_reg_map = { 1242 | 'pdi': ECT_REG_WD_TIME_PDI, 1243 | 'processdata': ECT_REG_WD_TIME_PROCESSDATA, 1244 | } 1245 | if wd_type not in wd_type_to_reg_map.keys(): 1246 | raise AttributeError() 1247 | wd_div_reg = int.from_bytes(self._fprd(ECT_REG_WD_DIV, 2, fprd_fpwr_timeout_us), 1248 | byteorder='little', 1249 | signed=False) 1250 | wd_div_ns = 40 * (wd_div_reg + 2) 1251 | wd_time_reg = int((wd_time_ms*1000000.0) / wd_div_ns) 1252 | if wd_time_reg > 0xFFFF: 1253 | wd_time_ms_limit = 0xFFFF * wd_div_ns / 1000000.0 1254 | raise AttributeError('wd_time_ms is limited to {} ms'.format(wd_time_ms_limit)) 1255 | actual_wd_time_ms = wd_time_reg * wd_div_ns / 1000000.0 1256 | self._fpwr(wd_type_to_reg_map[wd_type], 1257 | wd_time_reg.to_bytes(2, byteorder='little', signed=False), 1258 | fprd_fpwr_timeout_us) 1259 | 1260 | def add_emergency_callback(self, callback): 1261 | """Get notified on EMCY messages from this slave. 1262 | 1263 | :param callback: 1264 | Callable which must take one argument of an 1265 | :class:`~Emergency` instance. 1266 | """ 1267 | self._master.check_context_is_initialized() 1268 | self._emcy_callbacks.append(callback) 1269 | 1270 | cdef _on_emergency(self, cpysoem.ec_errort* emcy): 1271 | """Notify all emergency callbacks that an emergency message 1272 | was received. 1273 | 1274 | :param emcy: Emergency object. 1275 | """ 1276 | emergency_msg = Emergency(emcy.Slave, 1277 | emcy.ErrorCode, 1278 | emcy.ErrorReg, 1279 | emcy.b1, 1280 | emcy.w1, 1281 | emcy.w2) 1282 | for callback in self._emcy_callbacks: 1283 | callback(emergency_msg) 1284 | 1285 | def _disable_complete_access(self): 1286 | """Helper function that stops config_map() from using "complete access" for SDO requests for this device. 1287 | 1288 | This should only be used if your device has issues handling complete access requests but the CoE details of the 1289 | SII tells that SDO complete access is supported by the device. If you need this function something is wrong 1290 | with your device and you should contact the manufacturer about this issue. 1291 | 1292 | .. warning:: This is experimental. 1293 | 1294 | .. versionadded:: 1.1.3 1295 | """ 1296 | self._ec_slave.CoEdetails &= ~ECT_COEDET_SDOCA 1297 | 1298 | def _fprd(self, int address, int size, timeout_us=2000): 1299 | """Send and receive of the FPRD cmd primitive (Configured Address Physical Read).""" 1300 | cdef unsigned char* data 1301 | data = PyMem_Malloc(size) 1302 | cdef int wkc = cpysoem.ecx_FPRD(self._ecx_contextt.port, self._ec_slave.configadr, address, size, data, timeout_us) 1303 | if wkc != 1: 1304 | PyMem_Free(data) 1305 | raise WkcError() 1306 | try: 1307 | return PyBytes_FromStringAndSize(data, size) 1308 | finally: 1309 | PyMem_Free(data) 1310 | 1311 | def _fpwr(self, int address, bytes data, timeout_us=2000): 1312 | """Send and receive of the FPWR cmd primitive (Configured Address Physical Write).""" 1313 | cdef int wkc = cpysoem.ecx_FPWR(self._ecx_contextt.port, self._ec_slave.configadr, address, len(data), data, timeout_us) 1314 | if wkc != 1: 1315 | raise WkcError() 1316 | 1317 | cdef _raise_exception(self, cpysoem.ec_errort* err): 1318 | if err.Etype == cpysoem.EC_ERR_TYPE_SDO_ERROR: 1319 | raise SdoError(err.Slave, 1320 | err.Index, 1321 | err.SubIdx, 1322 | err.AbortCode, 1323 | cpysoem.ec_sdoerror2string(err.AbortCode).decode('utf8')) 1324 | elif err.Etype == cpysoem.EC_ERR_TYPE_EMERGENCY: 1325 | warnings.warn('This way of catching emergency messages is deprecated, use the add_emergency_callback() function!', FutureWarning) 1326 | raise Emergency(err.Slave, 1327 | err.ErrorCode, 1328 | err.ErrorReg, 1329 | err.b1, 1330 | err.w1, 1331 | err.w2) 1332 | elif err.Etype == cpysoem.EC_ERR_TYPE_MBX_ERROR: 1333 | raise MailboxError(err.Slave, 1334 | err.ErrorCode, 1335 | cpysoem.ec_mbxerror2string(err.ErrorCode).decode('utf8')) 1336 | elif err.Etype == cpysoem.EC_ERR_TYPE_PACKET_ERROR: 1337 | raise PacketError(err.Slave, 1338 | err.ErrorCode) 1339 | else: 1340 | raise Exception('unexpected error, Etype: {}'.format(err.Etype)) 1341 | 1342 | def _get_name(self): 1343 | """Name of the slave, read out from the slaves SII during config_init.""" 1344 | return (self._ec_slave.name).decode('utf8') 1345 | 1346 | def _get_eep_man(self): 1347 | """Vendor ID of the slave, read out from the slaves SII during config_init.""" 1348 | return self._ec_slave.eep_man 1349 | 1350 | def _get_eep_id(self): 1351 | """Product Code of the slave, read out from the slaves SII during config_init.""" 1352 | return self._ec_slave.eep_id 1353 | 1354 | def _get_eep_rev(self): 1355 | """Revision Number of the slave, read out from the slaves SII during config_init.""" 1356 | return self._ec_slave.eep_rev 1357 | 1358 | def _get_PO2SOconfig(self): 1359 | """Slaves callback function that is called during config_map. 1360 | 1361 | When the state changes from Pre-Operational state to Operational state.""" 1362 | if not self._ec_slave.user: 1363 | return None 1364 | 1365 | return self._ec_slave.user 1366 | 1367 | def _get_PO2SOconfigEx(self): 1368 | """Alternative callback function that is called during config_map. 1369 | 1370 | More precisely the function is called during the transition from Pre-Operational to Safe-Operational state. 1371 | Use this instead of the config_func. The difference is that the callbacks signature is fn(CdefSlave: slave). 1372 | 1373 | .. versionadded:: 1.1.0 1374 | """ 1375 | if not self._ec_slave.user: 1376 | return None 1377 | 1378 | return self._ec_slave.user 1379 | 1380 | def _set_PO2SOconfig(self, value): 1381 | self._cd.func = value 1382 | self._ec_slave.user = self._cd 1383 | if value is None: 1384 | self._ec_slave.PO2SOconfig = NULL 1385 | else: 1386 | self._ec_slave.PO2SOconfig = _xPO2SOconfig 1387 | 1388 | def _set_PO2SOconfigEx(self, value): 1389 | self._cd.func = value 1390 | self._ec_slave.user = self._cd 1391 | if value is None: 1392 | self._ec_slave.PO2SOconfig = NULL 1393 | else: 1394 | self._ec_slave.PO2SOconfig = _xPO2SOconfigEx 1395 | 1396 | def _get_state(self): 1397 | """Request a new state. 1398 | 1399 | After a new state has been set, `write_state` must be called. 1400 | """ 1401 | return self._ec_slave.state 1402 | 1403 | def _set_state(self, value): 1404 | self._ec_slave.state = value 1405 | 1406 | def _get_input(self): 1407 | num_bytes = self._ec_slave.Ibytes 1408 | if (self._ec_slave.Ibytes == 0 and self._ec_slave.Ibits > 0): 1409 | num_bytes = 1 1410 | return PyBytes_FromStringAndSize(self._ec_slave.inputs, num_bytes) 1411 | 1412 | def _get_output(self): 1413 | num_bytes = self._ec_slave.Obytes 1414 | if (self._ec_slave.Obytes == 0 and self._ec_slave.Obits > 0): 1415 | num_bytes = 1 1416 | return PyBytes_FromStringAndSize(self._ec_slave.outputs, num_bytes) 1417 | 1418 | def _set_output(self, bytes value): 1419 | memcpy(self._ec_slave.outputs, value, len(value)) 1420 | 1421 | def _get_al_status(self): 1422 | return self._ec_slave.ALstatuscode 1423 | 1424 | def _get_is_lost(self): 1425 | return self._ec_slave.islost 1426 | 1427 | def _set_is_lost(self, value): 1428 | self._ec_slave.islost = value 1429 | 1430 | def _get_od(self): 1431 | logger.debug('ecx_readODlist()') 1432 | cdef int result = cpysoem.ecx_readODlist(self._ecx_contextt, self._pos, &self._ex_odlist) 1433 | if not result > 0: 1434 | raise SdoInfoError('Sdo List Info read failed') 1435 | 1436 | coe_objects = [] 1437 | for i in range(self._ex_odlist.Entries): 1438 | coe_object = CdefCoeObject(i) 1439 | coe_object._ecx_context = self._ecx_contextt 1440 | coe_object._ex_odlist = &self._ex_odlist 1441 | coe_objects.append(coe_object) 1442 | 1443 | return coe_objects 1444 | 1445 | 1446 | 1447 | cdef class CdefCoeObject: 1448 | """Object info for objects in the object dictionary. 1449 | 1450 | Do not create instances of this class, you get instances of this type by the CdefSlave.od property. 1451 | """ 1452 | cdef cpysoem.ecx_contextt* _ecx_context 1453 | cdef cpysoem.ec_ODlistt* _ex_odlist 1454 | cdef int _item 1455 | cdef cpysoem.boolean _is_description_read 1456 | cdef cpysoem.boolean _are_entries_read 1457 | cdef cpysoem.ec_OElistt _ex_oelist 1458 | 1459 | index = property(_get_index) 1460 | data_type = property(_get_data_type) 1461 | name = property(_get_name) 1462 | object_code = property(_get_object_code) 1463 | entries = property(_get_entries) 1464 | bit_length = property(_get_bit_length) 1465 | obj_access = property(_get_obj_access) 1466 | 1467 | def __init__(self, int item): 1468 | self._item = item 1469 | self._is_description_read = False 1470 | self._are_entries_read = False 1471 | 1472 | def _read_description(self): 1473 | cdef int result 1474 | if not self._is_description_read: 1475 | logger.debug('ecx_readODdescription()') 1476 | result = cpysoem.ecx_readODdescription(self._ecx_context, self._item, self._ex_odlist) 1477 | if not result > 0: 1478 | raise SdoInfoError('Sdo Object Info read failed') 1479 | self._is_description_read = True 1480 | 1481 | def _read_entries(self): 1482 | cdef int result 1483 | if not self._are_entries_read: 1484 | logger.debug('ecx_readOE()') 1485 | result = cpysoem.ecx_readOE(self._ecx_context, self._item, self._ex_odlist, &self._ex_oelist) 1486 | if not result > 0: 1487 | raise SdoInfoError('Sdo ObjectEntry Info read failed') 1488 | self._are_entries_read = True 1489 | 1490 | def _get_index(self): 1491 | return self._ex_odlist.Index[self._item] 1492 | 1493 | def _get_data_type(self): 1494 | self._read_description() 1495 | return self._ex_odlist.DataType[self._item] 1496 | 1497 | def _get_object_code(self): 1498 | self._read_description() 1499 | return self._ex_odlist.ObjectCode[self._item] 1500 | 1501 | def _get_name(self): 1502 | self._read_description() 1503 | return self._ex_odlist.Name[self._item] 1504 | 1505 | def _get_entries(self): 1506 | self._read_description() 1507 | self._read_entries() 1508 | 1509 | if self._ex_odlist.MaxSub[self._item] == 0: 1510 | return [] 1511 | else: 1512 | entries = [] 1513 | for i in range(self._ex_odlist.MaxSub[self._item]+1): 1514 | entry = CdefCoeObjectEntry(i) 1515 | entry._ex_oelist = &self._ex_oelist 1516 | entries.append(entry) 1517 | return entries 1518 | 1519 | def _get_bit_length(self): 1520 | cdef int sum = 0 1521 | self._read_description() 1522 | self._read_entries() 1523 | if self._ex_odlist.MaxSub[self._item] == 0: 1524 | return self._ex_oelist.BitLength[0] 1525 | else: 1526 | for i in range(self._ex_odlist.MaxSub[self._item]+1): 1527 | sum += self._ex_oelist.BitLength[i] 1528 | return sum 1529 | 1530 | def _get_obj_access(self): 1531 | if self._ex_odlist.MaxSub[self._item] == 0: 1532 | return self._ex_oelist.ObjAccess[0] 1533 | else: 1534 | return 0 1535 | 1536 | 1537 | cdef class CdefCoeObjectEntry: 1538 | cdef cpysoem.ec_OElistt* _ex_oelist 1539 | cdef int _item 1540 | 1541 | name = property(_get_name) 1542 | data_type = property(_get_data_type) 1543 | bit_length = property(_get_bit_length) 1544 | obj_access = property(_get_obj_access) 1545 | 1546 | def __init__(self, int item): 1547 | self._item = item 1548 | 1549 | def _get_name(self): 1550 | return self._ex_oelist.Name[self._item] 1551 | 1552 | def _get_data_type(self): 1553 | return self._ex_oelist.DataType[self._item] 1554 | 1555 | def _get_bit_length(self): 1556 | return self._ex_oelist.BitLength[self._item] 1557 | 1558 | def _get_obj_access(self): 1559 | return self._ex_oelist.ObjAccess[self._item] 1560 | 1561 | 1562 | cdef int _xPO2SOconfig(cpysoem.uint16 slave, void* user) noexcept: 1563 | cdef _CallbackData cd 1564 | cd = user 1565 | cd.exc_raised = False 1566 | try: 1567 | (cd.func)(slave-1) 1568 | except: 1569 | cd.exc_raised = True 1570 | cd.exc_info = sys.exc_info() 1571 | 1572 | 1573 | cdef int _xPO2SOconfigEx(cpysoem.uint16 slave, void* user) noexcept: 1574 | cdef _CallbackData cd 1575 | cd = user 1576 | cd.exc_raised = False 1577 | try: 1578 | (cd.func)(cd.slave) 1579 | except: 1580 | cd.exc_raised = True 1581 | cd.exc_info = sys.exc_info() 1582 | -------------------------------------------------------------------------------- /src/soem/soem_config.c: -------------------------------------------------------------------------------- 1 | #include "soem_config.h" 2 | 3 | /* Default timeouts in us */ 4 | Ttimeouts soem_timeouts = { 5 | .ret = 2000, 6 | .safe = 20000, 7 | .eeprom = 20000, 8 | .tx_mailbox = 20000, 9 | .rx_mailbox = 700000, 10 | .state = 2000000 11 | }; 12 | -------------------------------------------------------------------------------- /src/soem/soem_config.h: -------------------------------------------------------------------------------- 1 | #ifndef _SOEM_CONFIG_H 2 | #define _SOEM_CONFIG_H 3 | 4 | typedef struct { 5 | int ret; 6 | int safe; 7 | int eeprom; 8 | int tx_mailbox; 9 | int rx_mailbox; 10 | int state; 11 | } Ttimeouts; 12 | 13 | extern Ttimeouts soem_timeouts; 14 | 15 | /** timeout value in us for tx frame to return to rx */ 16 | #define EC_TIMEOUTRET soem_timeouts.ret 17 | /** timeout value in us for safe data transfer, max. triple retry */ 18 | #define EC_TIMEOUTRET3 (EC_TIMEOUTRET * 3) 19 | /** timeout value in us for return "safe" variant (f.e. wireless) */ 20 | #define EC_TIMEOUTSAFE soem_timeouts.safe 21 | /** timeout value in us for EEPROM access */ 22 | #define EC_TIMEOUTEEP soem_timeouts.eeprom 23 | /** timeout value in us for tx mailbox cycle */ 24 | #define EC_TIMEOUTTXM soem_timeouts.tx_mailbox 25 | /** timeout value in us for rx mailbox cycle */ 26 | #define EC_TIMEOUTRXM soem_timeouts.rx_mailbox 27 | /** timeout value in us for check statechange */ 28 | #define EC_TIMEOUTSTATE soem_timeouts.state 29 | 30 | #endif /* _SOEM_CONFIG_H */ -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import time 4 | import threading 5 | import dataclasses 6 | import pytest 7 | 8 | import pysoem 9 | 10 | 11 | def pytest_addoption(parser): 12 | parser.addoption('--ifname', action='store') 13 | 14 | 15 | class PySoemTestEnvironment: 16 | """Setup a basic pysoem test fixture that is needed for most of the tests""" 17 | 18 | BECKHOFF_VENDOR_ID = 0x0002 19 | EK1100_PRODUCT_CODE = 0x044c2c52 20 | EL3002_PRODUCT_CODE = 0x0bba3052 21 | EL1259_PRODUCT_CODE = 0x04eb3052 22 | 23 | @dataclasses.dataclass 24 | class SlaveSet: 25 | name: str 26 | vendor_id: int 27 | product_code: int 28 | config_func: None 29 | 30 | def __init__(self, ifname): 31 | self._is_overlapping_enabled = None 32 | self._ifname = ifname 33 | self._master = pysoem.Master() 34 | self._master.in_op = False 35 | self._master.do_check_state = False 36 | self._proc_thread_handle = None 37 | self._check_thread_handle = None 38 | self._pd_thread_stop_event = threading.Event() 39 | self._ch_thread_stop_event = threading.Event() 40 | self._actual_wkc = 0 41 | 42 | self.el3002_config_func = None 43 | self.el1259_config_func = None 44 | self.el1259_setup_func = None 45 | 46 | self._expected_slave_layout = { 47 | 0: self.SlaveSet('XMC43-Test-Device', 0, 0x12783456, None), 48 | 1: self.SlaveSet('EK1100', self.BECKHOFF_VENDOR_ID, self.EK1100_PRODUCT_CODE, None), 49 | 2: self.SlaveSet('EL3002', self.BECKHOFF_VENDOR_ID, self.EL3002_PRODUCT_CODE, None), 50 | 3: self.SlaveSet('EL1259', self.BECKHOFF_VENDOR_ID, self.EL1259_PRODUCT_CODE, None), 51 | } 52 | 53 | def config_init(self): 54 | self._master.open(self._ifname) 55 | assert self._master.config_init(False) > 0 56 | 57 | def go_to_preop_state(self): 58 | self._master.state_check(pysoem.INIT_STATE, 50000) 59 | assert self._master.state == pysoem.SAFEOP_STATE 60 | 61 | self._proc_thread_handle = threading.Thread(target=self._processdata_thread) 62 | self._proc_thread_handle.start() 63 | self._check_thread_handle = threading.Thread(target=self._check_thread) 64 | self._check_thread_handle.start() 65 | 66 | self._master.write_state() 67 | for _ in range(400): 68 | self._master.state_check(pysoem.OP_STATE, 50000) 69 | if self._master.state == pysoem.OP_STATE: 70 | all_slaves_reached_op_state = True 71 | break 72 | assert 'all_slaves_reached_op_state' in locals(), 'could not reach OP state' 73 | self._master.in_op = True 74 | 75 | def config_map(self, overlapping_enable=False): 76 | self._is_overlapping_enabled = overlapping_enable 77 | 78 | # pull in the latest config_function into the _expected_slave_layout 79 | for device in self._expected_slave_layout.values(): 80 | if device.name == 'EL3002': 81 | device.config_func = self.el3002_config_func 82 | elif device.name == 'EL1259': 83 | device.config_func = self.el1259_config_func 84 | 85 | self._master.config_dc() 86 | for i, slave in enumerate(self._master.slaves): 87 | assert slave.man == self._expected_slave_layout[i].vendor_id 88 | assert slave.id == self._expected_slave_layout[i].product_code 89 | slave.config_func = self._expected_slave_layout[i].config_func 90 | # use the setup_func instead of the config_func 91 | if self._expected_slave_layout[i].name == 'EL1259' and self.el1259_setup_func is not None: 92 | slave.setup_func = self.el1259_setup_func 93 | slave.is_lost = False 94 | 95 | if self._is_overlapping_enabled: 96 | self._master.config_overlap_map() 97 | else: 98 | self._master.config_map() 99 | assert self._master.state_check(pysoem.SAFEOP_STATE) == pysoem.SAFEOP_STATE 100 | 101 | def go_to_op_state(self): 102 | self._master.state_check(pysoem.SAFEOP_STATE, 50000) 103 | assert self._master.state == pysoem.SAFEOP_STATE 104 | 105 | self._proc_thread_handle = threading.Thread(target=self._processdata_thread) 106 | self._proc_thread_handle.start() 107 | self._check_thread_handle = threading.Thread(target=self._check_thread) 108 | self._check_thread_handle.start() 109 | 110 | self._master.state = pysoem.OP_STATE 111 | self._master.write_state() 112 | for _ in range(400): 113 | self._master.state_check(pysoem.OP_STATE, 50000) 114 | if self._master.state == pysoem.OP_STATE: 115 | all_slaves_reached_op_state = True 116 | break 117 | assert 'all_slaves_reached_op_state' in locals(), 'could not reach OP state' 118 | self._master.in_op = True 119 | 120 | def teardown(self): 121 | self._pd_thread_stop_event.set() 122 | self._ch_thread_stop_event.set() 123 | if self._proc_thread_handle: 124 | self._proc_thread_handle.join() 125 | if self._check_thread_handle: 126 | self._check_thread_handle.join() 127 | 128 | self._master.state = pysoem.INIT_STATE 129 | self._master.write_state() 130 | self._master.close() 131 | 132 | def get_master(self): 133 | return self._master 134 | 135 | def get_slaves(self): 136 | return self._master.slaves 137 | 138 | def get_el1259(self): 139 | return self._master.slaves[3] 140 | 141 | def get_xmc_test_device(self): 142 | return self._master.slaves[0] # the XMC device 143 | 144 | def get_device_without_foe(self): 145 | return self._master.slaves[2] # the EL3002 146 | 147 | def _processdata_thread(self): 148 | while not self._pd_thread_stop_event.is_set(): 149 | if self._is_overlapping_enabled: 150 | self._master.send_overlap_processdata() 151 | else: 152 | self._master.send_processdata() 153 | self._actual_wkc = self._master.receive_processdata(10000) 154 | time.sleep(0.01) 155 | 156 | @staticmethod 157 | def _check_slave(slave, pos): 158 | if slave.state == (pysoem.SAFEOP_STATE + pysoem.STATE_ERROR): 159 | print( 160 | 'ERROR : slave {} is in SAFE_OP + ERROR, attempting ack.'.format(pos)) 161 | slave.state = pysoem.SAFEOP_STATE + pysoem.STATE_ACK 162 | slave.write_state() 163 | elif slave.state == pysoem.SAFEOP_STATE: 164 | print( 165 | 'WARNING : slave {} is in SAFE_OP, try change to OPERATIONAL.'.format(pos)) 166 | slave.state = pysoem.OP_STATE 167 | slave.write_state() 168 | elif slave.state > pysoem.NONE_STATE: 169 | if slave.reconfig(): 170 | slave.is_lost = False 171 | print('MESSAGE : slave {} reconfigured'.format(pos)) 172 | elif not slave.is_lost: 173 | slave.state_check(pysoem.OP_STATE) 174 | if slave.state == pysoem.NONE_STATE: 175 | slave.is_lost = True 176 | print('ERROR : slave {} lost'.format(pos)) 177 | if slave.is_lost: 178 | if slave.state == pysoem.NONE_STATE: 179 | if slave.recover(): 180 | slave.is_lost = False 181 | print( 182 | 'MESSAGE : slave {} recovered'.format(pos)) 183 | else: 184 | slave.is_lost = False 185 | print('MESSAGE : slave {} found'.format(pos)) 186 | 187 | def _check_thread(self): 188 | while not self._ch_thread_stop_event.is_set(): 189 | if self._master.in_op and ((self._actual_wkc < self._master.expected_wkc) or self._master.do_check_state): 190 | self._master.do_check_state = False 191 | self._master.read_state() 192 | for i, slave in enumerate(self._master.slaves): 193 | if slave.state != pysoem.OP_STATE: 194 | self._master.do_check_state = True 195 | self._check_slave(slave, i) 196 | if not self._master.do_check_state: 197 | print('OK : all slaves resumed OPERATIONAL.') 198 | time.sleep(0.01) 199 | 200 | 201 | @pytest.fixture 202 | def ifname(request): 203 | return request.config.getoption('--ifname') 204 | 205 | @pytest.fixture 206 | def pysoem_env(ifname): 207 | env = PySoemTestEnvironment(ifname) 208 | yield env 209 | env.teardown() 210 | -------------------------------------------------------------------------------- /tests/foe_testdata/random_data_01.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bnjmnp/pysoem/841ec5913c9da6101f4ca1a894cbf2bb305b54b5/tests/foe_testdata/random_data_01.bin -------------------------------------------------------------------------------- /tests/foe_testdata/random_data_02.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bnjmnp/pysoem/841ec5913c9da6101f4ca1a894cbf2bb305b54b5/tests/foe_testdata/random_data_02.bin -------------------------------------------------------------------------------- /tests/pysoem_basic_test.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | 3 | import pytest 4 | 5 | import pysoem 6 | 7 | BECKHOFF_VENDOR_ID = 0x0002 8 | EK1100_PRODUCT_CODE = 0x044C2C52 9 | EL3002_PRODUCT_CODE = 0x0BBA3052 10 | EL1259_PRODUCT_CODE = 0x04EB3052 11 | 12 | 13 | @dataclasses.dataclass 14 | class Device: 15 | name: str 16 | vendor_id: int 17 | product_code: int 18 | 19 | 20 | @pytest.fixture 21 | def revert_global_settings(): 22 | old_timeout_ret = pysoem.settings.timeouts.ret 23 | old_timeout_safe = pysoem.settings.timeouts.safe 24 | old_timeout_eeprom = pysoem.settings.timeouts.eeprom 25 | old_timeout_tx_mailbox = pysoem.settings.timeouts.tx_mailbox 26 | old_timeout_rx_mailbox = pysoem.settings.timeouts.rx_mailbox 27 | old_timeout_state = pysoem.settings.timeouts.state 28 | old_release_gil = pysoem.settings.always_release_gil 29 | 30 | yield None 31 | 32 | pysoem.settings.timeouts.ret = old_timeout_ret 33 | pysoem.settings.timeouts.safe = old_timeout_safe 34 | pysoem.settings.timeouts.eeprom = old_timeout_eeprom 35 | pysoem.settings.timeouts.tx_mailbox = old_timeout_tx_mailbox 36 | pysoem.settings.timeouts.rx_mailbox = old_timeout_rx_mailbox 37 | pysoem.settings.timeouts.state = old_timeout_state 38 | pysoem.settings.always_release_gil = old_release_gil 39 | 40 | 41 | 42 | def test_version(): 43 | assert isinstance(pysoem.__version__, str) 44 | 45 | 46 | def test_find_adapters(): 47 | pysoem.find_adapters() 48 | 49 | 50 | def test_config_function_exception(pysoem_env): 51 | """Check if an exception in a config_func callback causes config_map to fail""" 52 | 53 | class DummyException(Exception): 54 | pass 55 | 56 | def el1259_config_func(slave_pos): 57 | raise DummyException() 58 | 59 | pysoem_env.config_init() 60 | pysoem_env.el1259_config_func = el1259_config_func 61 | 62 | with pytest.raises(DummyException) as excinfo: 63 | pysoem_env.config_map() 64 | assert isinstance(excinfo.value, DummyException) 65 | 66 | 67 | def test_master_context_manager(ifname): 68 | """Quick check if the open() function context manager works as expected.""" 69 | with pysoem.open(ifname) as master: 70 | if master.config_init() > 0: 71 | expected_slave_layout = { 72 | 0: Device("XMC43-Test-Device", 0, 0x12783456), 73 | 1: Device("EK1100", BECKHOFF_VENDOR_ID, EK1100_PRODUCT_CODE), 74 | 2: Device("EL3002", BECKHOFF_VENDOR_ID, EL3002_PRODUCT_CODE), 75 | 3: Device("EL1259", BECKHOFF_VENDOR_ID, EL1259_PRODUCT_CODE), 76 | } 77 | for i, slave in enumerate(master.slaves): 78 | assert slave.man == expected_slave_layout[i].vendor_id 79 | assert slave.id == expected_slave_layout[i].product_code 80 | else: 81 | pytest.fail() 82 | 83 | 84 | setup_func_was_called = False 85 | 86 | 87 | def test_setup_function(pysoem_env): 88 | """Check if the new setup_func works as intended.""" 89 | 90 | def el1259_setup_func(slave): 91 | global setup_func_was_called 92 | assert slave.id == EL1259_PRODUCT_CODE 93 | setup_func_was_called = True 94 | 95 | pysoem_env.config_init() 96 | pysoem_env.el1259_setup_func = el1259_setup_func 97 | 98 | pysoem_env.config_map() # el1259_setup_func is expected to be called here 99 | 100 | assert setup_func_was_called 101 | 102 | 103 | def test_call_config_init_twice(pysoem_env): 104 | """In older versions of pysoem there was an issue calling config_init() multiple times. 105 | 106 | Every time config_init() was called again, the slaves list was extended not updated. 107 | """ 108 | pysoem_env.config_init() 109 | assert len(pysoem_env.get_master().slaves) == len(pysoem_env._expected_slave_layout) 110 | pysoem_env.config_init() 111 | assert len(pysoem_env.get_master().slaves) == len(pysoem_env._expected_slave_layout) 112 | 113 | 114 | def test_closed_interface_master(ifname): 115 | """Quick check if the open() function context manager works as expected.""" 116 | with pysoem.open(ifname) as master: 117 | if not master.config_init() > 0: 118 | pytest.fail() 119 | 120 | with pytest.raises(pysoem.NetworkInterfaceNotOpenError) as exec_info: 121 | master.send_processdata() 122 | 123 | 124 | def test_closed_interface_slave(ifname): 125 | """Quick check if the open() function context manager works as expected.""" 126 | with pysoem.open(ifname) as master: 127 | if master.config_init() > 0: 128 | slaves = master.slaves 129 | 130 | with pytest.raises(pysoem.NetworkInterfaceNotOpenError) as exec_info: 131 | slaves[0].sdo_read(0x1018, 1) 132 | 133 | 134 | def test_tune_timeouts(revert_global_settings): 135 | assert pysoem.settings.timeouts.ret == 2_000 136 | pysoem.settings.timeouts.ret = 5_000 137 | assert pysoem.settings.timeouts.ret == 5_000 138 | 139 | assert pysoem.settings.timeouts.safe == 20_000 140 | pysoem.settings.timeouts.safe = 70_000 141 | assert pysoem.settings.timeouts.safe == 70_000 142 | 143 | assert pysoem.settings.timeouts.eeprom == 20_000 144 | pysoem.settings.timeouts.eeprom = 30_000 145 | assert pysoem.settings.timeouts.eeprom == 30_000 146 | 147 | assert pysoem.settings.timeouts.tx_mailbox == 20_000 148 | pysoem.settings.timeouts.tx_mailbox = 90_000 149 | assert pysoem.settings.timeouts.tx_mailbox == 90_000 150 | 151 | assert pysoem.settings.timeouts.rx_mailbox == 700_000 152 | pysoem.settings.timeouts.rx_mailbox = 900_000 153 | assert pysoem.settings.timeouts.rx_mailbox == 900_000 154 | 155 | assert pysoem.settings.timeouts.state == 2_000_000 156 | pysoem.settings.timeouts.state = 5_000_000 157 | assert pysoem.settings.timeouts.state == 5_000_000 158 | 159 | 160 | def test_release_gil(revert_global_settings): 161 | assert pysoem.settings.always_release_gil == 0 162 | pysoem.settings.always_release_gil = True 163 | assert pysoem.settings.always_release_gil == 1 164 | 165 | master = pysoem.Master() 166 | assert master.always_release_gil == 1 167 | assert master.check_release_gil(None) == 1 168 | assert master.check_release_gil(True) == 1 169 | assert master.check_release_gil(False) == 0 170 | 171 | master.always_release_gil = False 172 | assert master.always_release_gil == 0 173 | assert pysoem.settings.always_release_gil == 1 174 | assert master.check_release_gil(None) == 0 175 | assert master.check_release_gil(True) == 1 176 | assert master.check_release_gil(False) == 0 177 | 178 | # New master would be created with pysoem.settings.always_release_gil value 179 | new_master = pysoem.Master() 180 | assert new_master.always_release_gil == 1 181 | assert master.always_release_gil == 0 182 | -------------------------------------------------------------------------------- /tests/pysoem_coe_test.py: -------------------------------------------------------------------------------- 1 | 2 | import struct 3 | import pytest 4 | import pysoem 5 | 6 | 7 | class EmergencyConsumer: 8 | def __init__(self): 9 | self._pending_emcy_msg = [] 10 | 11 | def on_emergency(self, emcy): 12 | self._pending_emcy_msg.append(emcy) 13 | 14 | def pop_emcy_msg(self): 15 | msg = self._pending_emcy_msg[-1] 16 | self._pending_emcy_msg = self._pending_emcy_msg[:-2] 17 | return msg 18 | 19 | 20 | @pytest.fixture 21 | def el1259(pysoem_env): 22 | pysoem_env.config_init() 23 | pysoem_env.config_map() 24 | return pysoem_env.get_el1259() 25 | 26 | 27 | @pytest.fixture 28 | def xmc_device(pysoem_env): 29 | pysoem_env.config_init() 30 | return pysoem_env.get_xmc_test_device() 31 | 32 | 33 | def get_obj_from_od(od, index): 34 | return next(obj for obj in od if obj.index == index) 35 | 36 | 37 | def test_sdo_read(el1259): 38 | """Validate that the returned object of sdo_read is of type byte.""" 39 | man_obj_bytes = el1259.sdo_read(0x1018, 1) 40 | assert type(man_obj_bytes) == bytes 41 | 42 | 43 | def test_access_not_existing_object(el1259): 44 | # read 45 | with pytest.raises(pysoem.SdoError) as excinfo: 46 | el1259.sdo_read(0x1111, 0, 1) 47 | assert excinfo.value.abort_code == 0x06020000 48 | assert excinfo.value.desc == 'The object does not exist in the object directory' 49 | 50 | # write 51 | with pytest.raises(pysoem.SdoError) as excinfo: 52 | el1259.sdo_write(0x1111, 0, bytes(4)) 53 | assert excinfo.value.abort_code == 0x06020000 54 | assert excinfo.value.desc == 'The object does not exist in the object directory' 55 | 56 | 57 | def test_write_a_ro_object(el1259): 58 | 59 | with pytest.raises(pysoem.SdoError) as excinfo: 60 | el1259.sdo_write(0x1008, 0, b'test') 61 | 62 | assert excinfo.value.abort_code == 0x08000021 63 | assert excinfo.value.desc == 'Data cannot be transferred or stored to the application because of local control' 64 | 65 | 66 | def test_compare_eeprom_against_coe_0x1018(el1259): 67 | sdo_man = struct.unpack('I', el1259.sdo_read(0x1018, 1))[0] 68 | assert sdo_man == el1259.man 69 | sdo_id = struct.unpack('I', el1259.sdo_read(0x1018, 2))[0] 70 | assert sdo_id == el1259.id 71 | sdo_rev = struct.unpack('I', el1259.sdo_read(0x1018, 3))[0] 72 | assert sdo_rev == el1259.rev 73 | 74 | sdo_sn = struct.unpack('I', el1259.sdo_read(0x1018, 4))[0] 75 | # serial number is expected to be at word address 0x0E 76 | eeprom_sn = struct.unpack('I', el1259.eeprom_read(0x0E))[0] 77 | assert sdo_sn == eeprom_sn 78 | 79 | 80 | def test_device_name(el1259): 81 | name_size = len(el1259.name) 82 | 83 | # test with given string size 84 | sdo_name = el1259.sdo_read(0x1008, 0, name_size).decode('utf-8') 85 | assert sdo_name == el1259.name 86 | 87 | # test without given string size 88 | sdo_name = el1259.sdo_read(0x1008, 0).decode('utf-8') 89 | assert sdo_name == el1259.name 90 | 91 | 92 | def test_read_buffer_to_small(el1259): 93 | with pytest.raises(pysoem.PacketError) as excinfo: 94 | el1259.sdo_read(0x1008, 0, 3).decode('utf-8') 95 | assert 4 == excinfo.value.slave_pos 96 | assert 3 == excinfo.value.error_code 97 | assert 'Data container too small for type', excinfo.value.desc 98 | 99 | 100 | def test_write_to_1c1x_while_in_safeop(el1259): 101 | for index in [0x1c12, 0x1c13]: 102 | with pytest.raises(pysoem.SdoError) as excinfo: 103 | el1259.sdo_write(index, 0, bytes(1)) 104 | assert excinfo.value.abort_code == 0x08000022 105 | assert excinfo.value.desc == 'Data cannot be transferred or stored to the application because of the present device state' 106 | 107 | 108 | @pytest.mark.skip 109 | def test_read_timeout(pysoem_env): 110 | """Test timeout 111 | 112 | TODO: test an object that really errors when the timeout is to low 113 | """ 114 | master = pysoem_env.get_master() 115 | old_sdo_read_timeout = master.sdo_read_timeout 116 | assert old_sdo_read_timeout == 700000 117 | master.sdo_read_timeout = 0 118 | assert master.sdo_read_timeout == 0 119 | master.sdo_read_timeout = old_sdo_read_timeout 120 | assert master.sdo_read_timeout == 700000 121 | 122 | 123 | @pytest.mark.skip 124 | def test_write_timeout(pysoem_env): 125 | """Test timeout 126 | 127 | TODO: test an object that really errors when the timeout is to low 128 | """ 129 | master = pysoem_env.get_master() 130 | old_sdo_write_timeout = master.sdo_write_timeout 131 | assert old_sdo_write_timeout == 700000 132 | master.sdo_write_timeout = 0 133 | assert master.sdo_write_timeout == 0 134 | master.sdo_write_timeout = old_sdo_write_timeout 135 | assert master.sdo_write_timeout == 700000 136 | 137 | 138 | def test_sdo_info_var(el1259): 139 | 140 | obj_0x1000 = get_obj_from_od(el1259.od, 0x1000) 141 | 142 | assert b'Device type' == obj_0x1000.name 143 | assert obj_0x1000.object_code == 7 144 | assert obj_0x1000.data_type == pysoem.ECT_UNSIGNED32 145 | assert obj_0x1000.bit_length == 32 146 | assert obj_0x1000.obj_access == 0x0007 147 | 148 | 149 | def test_sdo_info_rec(el1259): 150 | 151 | obj_0x1018 = get_obj_from_od(el1259.od, 0x1018) 152 | 153 | assert b'Identity' == obj_0x1018.name 154 | assert obj_0x1018.object_code == 9 155 | 156 | entry_vendor_id = obj_0x1018.entries[1] 157 | assert entry_vendor_id.name == b'Vendor ID' 158 | assert entry_vendor_id.data_type == pysoem.ECT_UNSIGNED32 159 | assert entry_vendor_id.bit_length == 32 160 | assert entry_vendor_id.obj_access == 0x0007 161 | 162 | 163 | @pytest.mark.parametrize('mode', ['mbx_receive', 'sdo_read']) 164 | def test_coe_emergency_legacy(xmc_device, mode): 165 | """Test if CoE Emergency errors can be received. 166 | 167 | The XMC device throws an CoE Emergency after writing to 0x8001:01. 168 | """ 169 | # no exception should be raise by mbx_receive() now. 170 | xmc_device.mbx_receive() 171 | # But this write should trigger an emergency message in the device, .. 172 | xmc_device.sdo_write(0x8001, 1, bytes(4)) 173 | # .. so ether an mbx_receive() or sdo_read() will reveal the emergency message. 174 | with pytest.warns(FutureWarning) as record: 175 | with pytest.raises(pysoem.Emergency) as excinfo: 176 | if mode == 'mbx_receive': 177 | xmc_device.mbx_receive() 178 | elif mode == 'sdo_read': 179 | _ = xmc_device.sdo_read(0x1018, 1) 180 | assert excinfo.value.slave_pos == 1 181 | assert excinfo.value.error_code == 0xFFFE 182 | assert excinfo.value.error_reg == 0x00 183 | assert excinfo.value.b1 == 0xAA 184 | assert excinfo.value.w1 == 0x5150 185 | assert excinfo.value.w2 == 0x5352 186 | assert str(excinfo.value) == 'Slave 1: fffe, 00, (aa,50,51,52,53)' 187 | assert len(record) == 1 188 | assert str(record[0].message) == 'This way of catching emergency messages is deprecated, use the add_emergency_callback() function!' 189 | # check if SDO communication is still working 190 | for i in range(10): 191 | _ = xmc_device.sdo_read(0x1018, 1) 192 | # again mbx_receive() should not raise any further exception 193 | xmc_device.mbx_receive() 194 | 195 | 196 | @pytest.mark.parametrize('mode', ['mbx_receive', 'sdo_read']) 197 | def test_coe_emergency_new(xmc_device, mode): 198 | emcy_consumer = EmergencyConsumer() 199 | xmc_device.add_emergency_callback(emcy_consumer.on_emergency) 200 | # no exception should be raise by mbx_receive() now. 201 | xmc_device.mbx_receive() 202 | assert len(emcy_consumer._pending_emcy_msg) == 0 203 | # But this write should trigger an emergency message in the device, .. 204 | xmc_device.sdo_write(0x8001, 1, bytes(4)) 205 | assert len(emcy_consumer._pending_emcy_msg) == 0 206 | # .. so ether an mbx_receive() or sdo_read() will reveal the emergency message. 207 | if mode == 'mbx_receive': 208 | xmc_device.mbx_receive() 209 | elif mode == 'sdo_read': 210 | _ = xmc_device.sdo_read(0x1018, 1) 211 | assert len(emcy_consumer._pending_emcy_msg) == 1 212 | emcy_msg = emcy_consumer.pop_emcy_msg() 213 | assert emcy_msg.slave_pos == 1 214 | assert emcy_msg.error_code == 0xFFFE 215 | assert emcy_msg.error_reg == 0x00 216 | assert emcy_msg.b1 == 0xAA 217 | assert emcy_msg.w1 == 0x5150 218 | assert emcy_msg.w2 == 0x5352 219 | assert len(emcy_consumer._pending_emcy_msg) == 0 220 | assert str(emcy_msg) == 'Slave 1: fffe, 00, (aa,50,51,52,53)' 221 | # check if SDO communication is still working 222 | for i in range(10): 223 | _ = xmc_device.sdo_read(0x1018, 1) 224 | assert len(emcy_consumer._pending_emcy_msg) == 0 225 | # again mbx_receive() should not raise any further exception 226 | xmc_device.mbx_receive() 227 | assert len(emcy_consumer._pending_emcy_msg) == 0 -------------------------------------------------------------------------------- /tests/pysoem_foe_test.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import pytest 4 | import pysoem 5 | 6 | 7 | test_dir = os.path.dirname(os.path.abspath(__file__)) 8 | 9 | 10 | def test_foe_good(pysoem_env): 11 | pysoem_env.config_init() 12 | test_slave = pysoem_env.get_xmc_test_device() 13 | 14 | for file_path in ['foe_testdata/random_data_01.bin', 'foe_testdata/random_data_02.bin']: 15 | with open(os.path.join(test_dir, file_path), 'rb') as file: 16 | random_data = file.read() 17 | 18 | # write 19 | test_slave.foe_write('test.bin', 0, random_data) 20 | # read back 21 | reread_data = test_slave.foe_read('test.bin', 0, 8192) 22 | # and check if the reread data is the same as the written data 23 | assert reread_data[:len(random_data)] == random_data 24 | 25 | 26 | def test_foe_fails(pysoem_env): 27 | pysoem_env.config_init() 28 | test_slave = pysoem_env.get_device_without_foe() 29 | 30 | # expect foe READ to fail 31 | with pytest.raises(pysoem.MailboxError) as excinfo: 32 | test_slave.foe_read('test.bin', 0, 8192) 33 | 34 | assert excinfo.value.error_code == 2 35 | assert excinfo.value.desc == 'The mailbox protocol is not supported' 36 | 37 | # expect foe WRITE to fail 38 | with pytest.raises(pysoem.MailboxError) as excinfo: 39 | test_slave.foe_write('test.bin', 0, bytes(32)) 40 | 41 | assert excinfo.value.error_code == 2 42 | assert excinfo.value.desc == 'The mailbox protocol is not supported' 43 | 44 | -------------------------------------------------------------------------------- /tests/pysoem_pdo_test.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import time 4 | import struct 5 | import pytest 6 | 7 | import pysoem 8 | 9 | 10 | class El1259ConfigFunction: 11 | 12 | def __init__(self, device, disable_complete_access=False): 13 | self._device = device 14 | self._disable_complete_access = disable_complete_access 15 | 16 | def fn(self, slave_pos): 17 | """ 18 | struct format characters 19 | B - uint8 20 | x - pac byte 21 | H - uint16 22 | """ 23 | 24 | self._device.sdo_write(0x8001, 2, struct.pack('B', 1)) 25 | 26 | rx_map_obj = [0x1603, 0x1607, 0x160B, 0x160F, 0x1613, 0x1617, 0x161B, 0x161F, 27 | 0x1620, 0x1621, 0x1622, 0x1623, 0x1624, 0x1625, 0x1626, 0x1627] 28 | pack_fmt = 'Bx' + ''.join(['H' for _ in range(len(rx_map_obj))]) 29 | rx_map_obj_bytes = struct.pack(pack_fmt, len(rx_map_obj), *rx_map_obj) 30 | self._device.sdo_write(0x1c12, 0, rx_map_obj_bytes, True) 31 | 32 | tx_map_obj = [0x1A00, 0x1A01, 0x1A02, 0x1A03, 0x1A04, 0x1A05, 0x1A06, 0x1A07, 0x1A08, 33 | 0x1A0C, 0x1A10, 0x1A14, 0x1A18, 0x1A1C, 0x1A20, 0x1A24] 34 | pack_fmt = 'Bx' + ''.join(['H' for _ in range(len(tx_map_obj))]) 35 | tx_map_obj_bytes = struct.pack(pack_fmt, len(tx_map_obj), *tx_map_obj) 36 | self._device.sdo_write(0x1c13, 0, tx_map_obj_bytes, True) 37 | 38 | self._device.dc_sync(1, 1000000) 39 | 40 | if self._disable_complete_access: 41 | self._device._disable_complete_access() 42 | 43 | 44 | @pytest.mark.parametrize('overlapping_enable', [False, True]) 45 | def test_io_toggle(pysoem_env, overlapping_enable): 46 | pysoem_env.config_init() 47 | el1259 = pysoem_env.get_el1259() 48 | pysoem_env.el1259_config_func = El1259ConfigFunction(el1259).fn 49 | pysoem_env.config_map(overlapping_enable) 50 | pysoem_env.go_to_op_state() 51 | 52 | output_len = len(el1259.output) 53 | 54 | tmp = bytearray([0 for _ in range(output_len)]) 55 | 56 | for i in range(8): 57 | out_offset = 12 * i 58 | in_offset = 4 * i 59 | 60 | tmp[out_offset] = 0x02 61 | el1259.output = bytes(tmp) 62 | time.sleep(0.1) 63 | assert el1259.input[in_offset] & 0x04 == 0x04 64 | 65 | tmp[out_offset] = 0x00 66 | el1259.output = bytes(tmp) 67 | time.sleep(0.1) 68 | assert el1259.input[in_offset] & 0x04 == 0x00 69 | 70 | 71 | @pytest.mark.parametrize('disable_complete_access', [False, True]) 72 | def test_disable_complete_access(pysoem_env, disable_complete_access): 73 | """Very basic sanity check if disable_complete_access does not do any damage.""" 74 | pysoem_env.config_init() 75 | el1259 = pysoem_env.get_el1259() 76 | pysoem_env.el1259_config_func = El1259ConfigFunction(el1259, disable_complete_access).fn 77 | pysoem_env.config_map() 78 | pysoem_env.go_to_op_state() 79 | time.sleep(1) -------------------------------------------------------------------------------- /tests/pysoem_register_test.py: -------------------------------------------------------------------------------- 1 | """Tests for pysoem functions that are based on register level communication.""" 2 | import pytest 3 | 4 | 5 | @pytest.fixture 6 | def watchdog_device(pysoem_env): 7 | pysoem_env.config_init() 8 | return pysoem_env.get_xmc_test_device() 9 | 10 | 11 | @pytest.fixture 12 | def watchdog_register_fix(watchdog_device): 13 | timeout_ms = 4000 14 | assert watchdog_device._fprd(0x400, 2, timeout_ms) == bytes([0xC2, 0x09]) 15 | old_wd_time_pdi = watchdog_device._fprd(0x410, 2, timeout_ms) 16 | old_wd_time_processdata = watchdog_device._fprd(0x420, 2, timeout_ms) 17 | yield None 18 | watchdog_device._fpwr(0x410, old_wd_time_pdi, timeout_ms) 19 | watchdog_device._fpwr(0x420, old_wd_time_processdata, timeout_ms) 20 | 21 | 22 | @pytest.mark.parametrize('wd', ['pdi', 'processdata']) 23 | @pytest.mark.parametrize('time_ms,expected_reg_value', [ 24 | (100, 1000), 25 | (10, 100), 26 | (1, 10), 27 | (0.1, 1), 28 | (0.15, 1), 29 | (0.2555, 2), 30 | (0, 0), 31 | (6553.6, AttributeError()), 32 | ]) 33 | def test_watchdog_update(watchdog_device, watchdog_register_fix, wd, time_ms, expected_reg_value): 34 | """Test the set_watchdog() function of the CdefSlave object.""" 35 | wd_reg = { 36 | 'pdi': 0x410, 37 | 'processdata': 0x420, 38 | } 39 | if isinstance(expected_reg_value, AttributeError): 40 | with pytest.raises(AttributeError): 41 | watchdog_device.set_watchdog(wd_type=wd, wd_time_ms=time_ms) 42 | else: 43 | watchdog_device.set_watchdog(wd_type=wd, wd_time_ms=time_ms) 44 | assert watchdog_device._fprd(address=wd_reg[wd], size=2, timeout_us=4000) == expected_reg_value.to_bytes(2, byteorder='little', signed=False) 45 | -------------------------------------------------------------------------------- /tests/tox_local.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | requires = 3 | tox>=4 4 | env_list = py{37,38,39,310,311,312,313} 5 | 6 | [testenv] 7 | description = test a just locally build pysoem 8 | deps = 9 | pytest 10 | commands_pre = 11 | python -I -m pip install .. 12 | commands = 13 | pytest {posargs} -------------------------------------------------------------------------------- /tests/tox_pypi.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | requires = 3 | tox>=4 4 | env_list = py{37,38,39,310,311,312,313} 5 | 6 | [testenv] 7 | description = test a freshly downloaded pysoem from PyPI 8 | deps = 9 | pytest 10 | pysoem 11 | commands = 12 | pytest {posargs} -------------------------------------------------------------------------------- /tests/tox_test_pypi.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | requires = 3 | tox>=4 4 | env_list = py{37,38,39,310,311,312,313} 5 | 6 | [testenv] 7 | description = test a freshly downloaded pysoem from TestPyPI 8 | deps = 9 | pytest 10 | commands_pre = 11 | python -I -m pip install -i https://test.pypi.org/simple/ pysoem 12 | commands = 13 | pytest {posargs} --------------------------------------------------------------------------------