├── .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