├── .gitignore ├── .gitmodules ├── COPYING ├── MANIFEST.in ├── README.rst ├── debian ├── .gitignore ├── changelog ├── compat ├── control ├── copyright ├── gsdparser.install ├── gsdparser.manpages ├── profisniff.install ├── profisniff.manpages ├── python3-pyprofibus.install ├── rules └── source │ └── format ├── doc ├── baudrates.ods ├── hardware.rst ├── hardware_fpga.rst ├── man │ ├── gsdparser.1 │ ├── profisniff.1 │ └── pyprofibus-linuxcnc-hal.1 ├── pb_frame_format.dia ├── pb_frame_format.png ├── pb_telegram_format.dia ├── pb_telegram_format.png └── ttlserial_to_rs485 │ ├── .gitignore │ ├── ttlserial_to_rs485-cache.lib │ ├── ttlserial_to_rs485.kicad_pcb │ ├── ttlserial_to_rs485.pdf │ ├── ttlserial_to_rs485.pro │ └── ttlserial_to_rs485.sch ├── examples ├── example_dummy_inputonly.conf ├── example_dummy_inputonly.py ├── example_dummy_oneslave.conf ├── example_dummy_oneslave.py ├── example_dummy_twoslaves.conf ├── example_dummy_twoslaves.py ├── example_et200s.conf ├── example_et200s.py ├── example_s7_315_2dp.conf ├── example_s7_315_2dp.py └── linuxcnc │ ├── dummy_phy │ ├── .gitignore │ ├── dummy.gsd │ ├── linuxcnc-demo.hal │ ├── linuxcnc-demo.ini │ ├── linuxcnc-demo.ngc │ ├── profibus.hal │ ├── pyprofibus.conf │ └── run-linuxcnc-demo.sh │ └── et200s │ ├── .gitignore │ ├── linuxcnc-demo.hal │ ├── linuxcnc-demo.ini │ ├── linuxcnc-demo.ngc │ ├── profibus.hal │ ├── pyprofibus.conf │ └── run-linuxcnc-demo.sh ├── gsdparser ├── maintenance ├── gen-doc.sh ├── makerelease.sh └── update-submodules ├── micropython ├── Makefile ├── README.rst ├── boot.py ├── install.sh └── main.py ├── misc ├── dummy_compact.gsd ├── dummy_modular.gsd └── setup_cython.py ├── phy_fpga ├── .gitignore ├── Makefile ├── block_ram_mod.v ├── edge_detect_mod.v ├── led_blink_mod.v ├── main.v ├── parity_func.v ├── profibus_phy_mod.v ├── spi_slave_mod.v ├── sync_signal_mod.v ├── tinyfpga_bx.pcf ├── tinyfpga_bx_program.sh └── uart_mod.v ├── profisniff ├── pyprofibus-linuxcnc-hal ├── pyprofibus ├── __init__.py ├── compat.py ├── conf.py ├── dp.py ├── dp_master.py ├── fdl.py ├── gsd │ ├── __init__.py │ ├── fields.py │ ├── interp.py │ └── parser.py ├── phy.py ├── phy_dummy.py ├── phy_fpga.py ├── phy_fpga_driver │ ├── __init__.py │ ├── driver.py │ ├── exceptions.py │ ├── io.py │ └── messages.py ├── phy_serial.py ├── util.py └── version.py ├── setup.py ├── stublibs ├── README.rst ├── __future__.py ├── collections.py ├── configparser.py ├── difflib.py └── serial.py └── tests ├── __init__.py ├── pyprofibus_tstlib.py ├── run.sh ├── test_dummy.py └── test_gsd.py /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .pybuild/ 3 | 4 | __pycache__/ 5 | *.pyc 6 | *.pyo 7 | 8 | .*.swp 9 | .~* 10 | 11 | *.html 12 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "phy_fpga/crcgen"] 2 | path = phy_fpga/crcgen 3 | url = https://git.bues.ch/git/crcgen.git 4 | [submodule "phy_fpga/fpgamakelib"] 5 | path = phy_fpga/fpgamakelib 6 | url = https://git.bues.ch/git/fpgamakelib.git 7 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft debian 2 | graft doc 3 | graft examples 4 | graft micropython 5 | graft misc 6 | graft phy_fpga 7 | graft stublibs 8 | graft tests 9 | 10 | include COPYING 11 | include README.html 12 | include README.md 13 | include README.rst 14 | 15 | prune ./build 16 | prune build 17 | 18 | global-exclude *.pyo *.pyc __pycache__ *$py.class 19 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | pyprofibus - PROFIBUS-DP stack 2 | ============================== 3 | 4 | pyprofibus is an Open Source `PROFIBUS-DP `_ stack written in Python. 5 | 6 | `pyprofibus home `_ 7 | 8 | `Git repository `_ 9 | 10 | `Github repository `_ 11 | 12 | 13 | Hardware 14 | ======== 15 | 16 | pyprofibus is able to run on any machine that supports Python. It also runs on embedded machines such as the `Raspberry Pi `_ or even tiny microcontrollers such as the `ESP32 `_ (Micropython). 17 | 18 | Please read the hardware documentation for more information: 19 | 20 | `pyprofibus hardware documentation `_ 21 | 22 | 23 | Speed / Baud rate 24 | ================= 25 | 26 | The achievable Profibus-DP speed depends on the hardware that it runs on and what kind of serial transceiver is used. There is no software side artificial limit. 27 | 28 | Please see the `pyprofibus hardware documentation `_ 29 | 30 | 31 | Examples 32 | ======== 33 | 34 | pyprofibus comes with a couple of examples that can teach you how to use pyprofibus in your project. 35 | 36 | * Example that runs pyprofibus without any hardware. This example can be used to play around with pyprofibus. 37 | * examples/example_dummy_oneslave.py 38 | * examples/example_dummy_oneslave.conf 39 | * examples/example_dummy_twoslaves.py 40 | * examples/example_dummy_twoslaves.conf 41 | * examples/example_dummy_inputonly.py 42 | * examples/example_dummy_inputonly.conf 43 | 44 | * Example that runs pyprofibus as master connected to an ET200S as slave. 45 | * examples/example_et200s.py 46 | * examples/example_et200s.conf 47 | 48 | * Example that runs pyprofibus as master connected to an S7-315-2DP as *slave*. 49 | * examples/example_s7-315-2dp.py 50 | * examples/example_s7-315-2dp.conf 51 | 52 | 53 | Dependencies 54 | ============ 55 | 56 | * `Python `_ 3.5 or later. 57 | * Or alternatively `Micropython `_. Please see the `pyprofibus Micropython help `_ for more information. 58 | 59 | 60 | License 61 | ======= 62 | 63 | Copyright (c) 2013-2024 Michael Büsch 64 | 65 | Licensed under the terms of the GNU General Public License version 2, or (at your option) any later version. 66 | -------------------------------------------------------------------------------- /debian/.gitignore: -------------------------------------------------------------------------------- 1 | /destdir-*/ 2 | /gsdparser/ 3 | /profisniff/ 4 | /python-pyprofibus*/ 5 | /python3-pyprofibus*/ 6 | 7 | /debhelper-build-stamp 8 | /files 9 | *.debhelper 10 | *.log 11 | *.substvars 12 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | pyprofibus (1.13) UNRELEASED; urgency=low 2 | 3 | * Version 1.13 4 | 5 | -- Michael Buesch Mon, 01 Feb 2016 00:00:00 +0100 6 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 10 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: pyprofibus 2 | Maintainer: Michael Buesch 3 | Section: python 4 | Priority: optional 5 | Build-Depends: dh-python, 6 | python3-dev (>= 3.7), 7 | python3-setuptools, 8 | debhelper (>= 10) 9 | Standards-Version: 4.5.1 10 | X-Python3-Version: >= 3.7 11 | Vcs-Git: git://git.bues.ch/pyprofibus.git 12 | Vcs-Browser: https://bues.ch/gitweb?p=pyprofibus.git 13 | Homepage: https://bues.ch/a/profibus 14 | 15 | Package: python3-pyprofibus 16 | Architecture: any 17 | Depends: ${misc:Depends}, 18 | ${python3:Depends} 19 | Description: PROFIBUS-DP stack (Python 3) 20 | pyprofibus is a PROFIBUS-DP stack written in Python. 21 | 22 | Package: profisniff 23 | Architecture: any 24 | Depends: python3-pyprofibus, 25 | ${misc:Depends}, 26 | ${python3:Depends} 27 | Description: PROFIBUS-DP telegram sniffer (Python 3) 28 | PROFIBUS-DP telegram sniffer based on pyprofibus. 29 | 30 | Package: gsdparser 31 | Architecture: any 32 | Depends: python3-pyprofibus, 33 | ${misc:Depends}, 34 | ${python3:Depends} 35 | Description: PROFIBUS-DP GSD file parser (Python 3) 36 | PROFIBUS-DP GSD file parser based on pyprofibus. 37 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | 3 | Files: * 4 | Copyright: 2013-2016 Michael Buesch 5 | License: GPL-2+ 6 | /usr/share/common-licenses/GPL-2 7 | -------------------------------------------------------------------------------- /debian/gsdparser.install: -------------------------------------------------------------------------------- 1 | debian/destdir-py3-pyprofibus/usr/bin/gsdparser usr/bin 2 | -------------------------------------------------------------------------------- /debian/gsdparser.manpages: -------------------------------------------------------------------------------- 1 | doc/man/gsdparser.1 2 | -------------------------------------------------------------------------------- /debian/profisniff.install: -------------------------------------------------------------------------------- 1 | debian/destdir-py3-pyprofibus/usr/bin/profisniff usr/bin 2 | -------------------------------------------------------------------------------- /debian/profisniff.manpages: -------------------------------------------------------------------------------- 1 | doc/man/profisniff.1 2 | -------------------------------------------------------------------------------- /debian/python3-pyprofibus.install: -------------------------------------------------------------------------------- 1 | debian/destdir-py3-pyprofibus/usr/lib/python3*/dist-packages/pyprofibus usr/lib/python3/dist-packages/ 2 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | export LC_ALL=C.UTF-8 4 | #export DH_VERBOSE=1 5 | 6 | export PYBUILD_NAME=pyprofibus 7 | export PYBUILD_SYSTEM=distutils 8 | #export PYBUILD_DEBUG=1 9 | 10 | export PYBUILD_DESTDIR_python3=debian/destdir-py3-pyprofibus/ 11 | export PYBUILD_DESTDIR_python3-dbg=debian/destdir-py3-pyprofibus-dbg/ 12 | 13 | export PYTHONDONTWRITEBYTECODE=1 14 | export PYTHONPATH= 15 | export PYTHONSTARTUP= 16 | export PYTHONINSPECT= 17 | 18 | export PYPROFIBUS_CYTHON_BUILD=0 19 | export PYPROFIBUS_CYTHON_PARALLEL=1 20 | 21 | %: 22 | dh $@ --with python3 --buildsystem=pybuild 23 | 24 | override_dh_auto_test: 25 | true 26 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /doc/baudrates.ods: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbuesch/pyprofibus/e7c981589392b09720940538eca83a78e2cd68f8/doc/baudrates.ods -------------------------------------------------------------------------------- /doc/hardware.rst: -------------------------------------------------------------------------------- 1 | pyprofibus hardware documentation 2 | ================================= 3 | 4 | pyprofibus can run on about anything that has a serial port. 5 | 6 | However some hardware is superior to other. See the documentation below for the various hardware alternatives and its characteristics. 7 | 8 | 9 | pyprofibus on Linux with /dev/ttyS0 or /dev/ttyAMA0 serial port 10 | =============================================================== 11 | 12 | Using the Linux serial port is a supported way to connect pyprofibus to a Profibus network. On some boards it may only supports low baud rates of up to about 19200 baud. However that depends on the actual serial transceiver hardware. On certain embedded boards with flexible serial hardware, high baudrates such as 1.5 MBaud may also be possible. 13 | 14 | To run pyprofibus on serial port configure pyprofibus as follows: 15 | 16 | .. code:: ini 17 | 18 | [PHY] 19 | type=serial 20 | dev=/dev/ttyS0 21 | baud=19200 22 | 23 | pyprofibus on Linux with /dev/ttyUSB0 serial port 24 | ================================================= 25 | 26 | It is not recommended to run pyprofibus on an emulated USB serial adapter. USB does not meet the realtime requirements of Profibus. It might work with slow baud rates, though. Use without any guarantee. 27 | 28 | 29 | pyprofibus on ESP32 with Micropython 30 | ==================================== 31 | 32 | Pyprofibus on ESP32 with Micropython supports baud rates of at least 1.5 MBaud. 33 | 34 | Please see the `pyprofibus Micropython help <../micropython/README.rst>`_. 35 | 36 | To run pyprofibus on Micropython serial port (UART 2) configure pyprofibus as follows: 37 | 38 | .. code:: ini 39 | 40 | [PHY] 41 | type=serial 42 | dev=UART2 43 | baud=1500000 44 | 45 | 46 | pyprofibus on Linux with FPGA PHY 47 | ================================= 48 | 49 | This is one of the fastest albeit most expensive alternative to connect pyprofibus to a Profibus network. Currently baud rates of up to 1.5 MBaud are supported. There is room for improvement towards higher baud rates. 50 | 51 | The pyprofibus FPGA is connected via high speed SPI bus to the host computer. It's known to work well with the Raspberry Pi. However it's not strictly limited to that as host computer. The pyprofibus FPGA PHY driver utilizes the Linux SPI subsystem for communication to the FPGA board. 52 | 53 | Please see the `pyprofibus FPGA PHY documentation `_ for more information on how to build and run the FPGA PHY. 54 | 55 | To run pyprofibus on FPGA PHY configure pyprofibus as follows: 56 | 57 | .. code:: ini 58 | 59 | [PHY] 60 | type=fpga 61 | spiBus=0 62 | spiCS=0 63 | spiSpeedHz=2500000 64 | baud=1500000 65 | 66 | The FPGA PHY is currently not supported on Micropython. 67 | 68 | 69 | pyprofibus on MS Windows 70 | ======================== 71 | 72 | pyprofibus has been reported to work on Windows with the `serial` PHY. Just use the COM1 / COM2 / COMx as `dev=` in the configuration. The same restrictions apply as with Linux `serial` PHY. Please the the Linux /dev/ttyS0 section. 73 | -------------------------------------------------------------------------------- /doc/hardware_fpga.rst: -------------------------------------------------------------------------------- 1 | pyprofibus FPGA PHY documentation 2 | ================================= 3 | 4 | Instead of using the serial port of the host computer to connect to the Profibus network an FPGA can be used. That improves the realtime capabilities and the speed of the pyprofibus system. 5 | 6 | 7 | FPGA boards 8 | =========== 9 | 10 | Currently only the `TinyFPGA BX `_ is supported as FPGA. 11 | 12 | However it's possible to add support for other FPGAs. The TinyFPGA BX has been chosen, because it has a fully Open Source toolchain. So the pyprofibus FPGA Verilog sources can be translated to the binary FPGA bitstream using only freely available tools. 13 | 14 | The pyprofibus releases come with pre-built bitstream images. So there's no need for the user to install the toolchain and build the FPGA bitstream. The only tool that is needed is the "flashing/programming tool" that is used to transfer the binary bitstream to the FPGA board. 15 | 16 | 17 | Flashing / Programming tool 18 | --------------------------- 19 | 20 | For the purpose of writing/downloading/flashing the binary FPGA bitstream to the TinyFPGA BX board the `tinyprog `_ tool can be used. 21 | 22 | The helper script `tinyfpga_bx_program.sh` shipped with pyprofibus in the subdirectory `phy_fpga/bin/tinyfpga_bx` can be used to conveniently call `tinyprog` with the correct parameters. Just connect the TinyFPGA BX board via USB to the computer and run the `tinyfpga_bx_program.sh` script. It does everything to program the pyprofibus PHY to your TinyFPGA BX. 23 | 24 | Build toolchain 25 | --------------- 26 | 27 | If you want to modify the FPGA sources and build your own version of the FPGA bitstream, the following tools are required: 28 | 29 | 30 | Project IceStorm 31 | ~~~~~~~~~~~~~~~~ 32 | 33 | The `Project IceStorm `_ is needed to build the bitstream for the TinyFPGA BX board. 34 | 35 | 36 | Yosys 37 | ~~~~~ 38 | 39 | The `Yosys Open SYnthesis Suite `_ is required to synthesize the Verilog sources. 40 | 41 | 42 | nextpnr 43 | ~~~~~~~ 44 | 45 | The `nextpnr FPGA place and route tool `_ is required for FPGA routing. 46 | 47 | 48 | Python 49 | ~~~~~~ 50 | 51 | `Python 3.7 `_ or later is required to auto-generate some parts of the sources. 52 | 53 | 54 | GNU make an other standard GNU/Linux shell utilities 55 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 56 | 57 | `GNU make `_ and various other standard GNU/Linux shell utilities, that are already installed on any Desktop Linux distribution. Building on Windows is not supported. 58 | 59 | 60 | Downloading and installing all toolchain tools 61 | ---------------------------------------------- 62 | 63 | The script `build_fpga_toolchain.sh` that is included in the pyprofibus archive can be used to download, build and install all FPGA toolchain tools to the `$HOME` directory. 64 | 65 | Just run the script as follows. It will download all required packages, build and install everything to `$HOME/fpga-toolchain` by default. 66 | 67 | .. code:: sh 68 | 69 | cd phy_fpga/fpgamakelib 70 | ./build_fpga_toolchain.sh 71 | 72 | After successful execution of `build_fpga_toolchain.sh` please read the information about `$PATH` that the script prints to the console. The line printed by the script shall be added to your `$HOME/.bashrc` 73 | 74 | No changes to the operating system are necessary. Do *not* run `build_fpga_toolchain.sh` as root. 75 | 76 | The toolchain can be uninstalled by simply deleting the directory `$HOME/fpga-toolchain` 77 | 78 | Building pyprofibus PHY-FPGA 79 | ---------------------------- 80 | 81 | To build the PHY-FPGA sources run the following commands: 82 | 83 | .. code:: sh 84 | 85 | cd phy_fpga 86 | make clean 87 | make 88 | -------------------------------------------------------------------------------- /doc/man/gsdparser.1: -------------------------------------------------------------------------------- 1 | .TH GSDPARSER "1" "2016" "gsdparser" "User Commands" 2 | .SH NAME 3 | gsdparser \- PROFIBUS-DP GSD file parser based on pyprofibus 4 | .SH SYNOPSIS 5 | .B gsdparser 6 | [\fIOPTIONS\fR] 7 | .SH DESCRIPTION 8 | PROFIBUS-DP GSD file parser based on pyprofibus 9 | .SH OPTIONS 10 | .TP 11 | \fB\-h\fR|\-\-help 12 | Print help information 13 | .SH AUTHORS 14 | Michael Buesch 15 | -------------------------------------------------------------------------------- /doc/man/profisniff.1: -------------------------------------------------------------------------------- 1 | .TH PROFISNIFF "1" "2016" "profisniff" "User Commands" 2 | .SH NAME 3 | profisniff \- PROFIBUS-DP telegramm sniffer based on pyprofibus 4 | .SH SYNOPSIS 5 | .B profisniff 6 | [\fIOPTIONS\fR] 7 | .SH DESCRIPTION 8 | PROFIBUS-DP telegramm sniffer based on pyprofibus 9 | .SH OPTIONS 10 | .TP 11 | \fB\-h\fR|\-\-help 12 | Print help information 13 | .SH AUTHORS 14 | Michael Buesch 15 | -------------------------------------------------------------------------------- /doc/man/pyprofibus-linuxcnc-hal.1: -------------------------------------------------------------------------------- 1 | .TH PYPROFIBUS-LINUXCNC-HAL "1" "2016" "pyprofibus-linuxcnc-hal" "User Commands" 2 | .SH NAME 3 | pyprofibus-linuxcnc-hal \- PROFIBUS-DP HAL module for LinuxCNC based on pyprofibus 4 | .SH SYNOPSIS 5 | .B pyprofibus-linuxcnc-hal 6 | [\fIOPTIONS\fR] 7 | .SH DESCRIPTION 8 | PROFIBUS-DP HAL module for LinuxCNC based on pyprofibus 9 | .SH OPTIONS 10 | .TP 11 | \fB\-h\fR|\-\-help 12 | Print help information 13 | .SH AUTHORS 14 | Michael Buesch 15 | -------------------------------------------------------------------------------- /doc/pb_frame_format.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbuesch/pyprofibus/e7c981589392b09720940538eca83a78e2cd68f8/doc/pb_frame_format.dia -------------------------------------------------------------------------------- /doc/pb_frame_format.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbuesch/pyprofibus/e7c981589392b09720940538eca83a78e2cd68f8/doc/pb_frame_format.png -------------------------------------------------------------------------------- /doc/pb_telegram_format.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbuesch/pyprofibus/e7c981589392b09720940538eca83a78e2cd68f8/doc/pb_telegram_format.dia -------------------------------------------------------------------------------- /doc/pb_telegram_format.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbuesch/pyprofibus/e7c981589392b09720940538eca83a78e2cd68f8/doc/pb_telegram_format.png -------------------------------------------------------------------------------- /doc/ttlserial_to_rs485/.gitignore: -------------------------------------------------------------------------------- 1 | *.sch-bak 2 | -------------------------------------------------------------------------------- /doc/ttlserial_to_rs485/ttlserial_to_rs485.kicad_pcb: -------------------------------------------------------------------------------- 1 | (kicad_pcb (version 4) (host kicad "dummy file") ) 2 | -------------------------------------------------------------------------------- /doc/ttlserial_to_rs485/ttlserial_to_rs485.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbuesch/pyprofibus/e7c981589392b09720940538eca83a78e2cd68f8/doc/ttlserial_to_rs485/ttlserial_to_rs485.pdf -------------------------------------------------------------------------------- /doc/ttlserial_to_rs485/ttlserial_to_rs485.pro: -------------------------------------------------------------------------------- 1 | update=Wed 29 Jul 2020 07:05:15 PM CEST 2 | version=1 3 | last_client=kicad 4 | [general] 5 | version=1 6 | RootSch= 7 | BoardNm= 8 | [pcbnew] 9 | version=1 10 | LastNetListRead= 11 | UseCmpFile=1 12 | PadDrill=0.600000000000 13 | PadDrillOvalY=0.600000000000 14 | PadSizeH=1.500000000000 15 | PadSizeV=1.500000000000 16 | PcbTextSizeV=1.500000000000 17 | PcbTextSizeH=1.500000000000 18 | PcbTextThickness=0.300000000000 19 | ModuleTextSizeV=1.000000000000 20 | ModuleTextSizeH=1.000000000000 21 | ModuleTextSizeThickness=0.150000000000 22 | SolderMaskClearance=0.000000000000 23 | SolderMaskMinWidth=0.000000000000 24 | DrawSegmentWidth=0.200000000000 25 | BoardOutlineThickness=0.100000000000 26 | ModuleOutlineThickness=0.150000000000 27 | [cvpcb] 28 | version=1 29 | NetIExt=net 30 | [eeschema] 31 | version=1 32 | LibDir= 33 | [eeschema/libraries] 34 | [schematic_editor] 35 | version=1 36 | PageLayoutDescrFile= 37 | PlotDirectoryName= 38 | SubpartIdSeparator=0 39 | SubpartFirstId=65 40 | NetFmtName=Pcbnew 41 | SpiceAjustPassiveValues=0 42 | LabSize=50 43 | ERC_TestSimilarLabels=1 44 | -------------------------------------------------------------------------------- /examples/example_dummy_inputonly.conf: -------------------------------------------------------------------------------- 1 | ; ----------------------------------------------- ; 2 | ; ; 3 | ; PROFIBUS configuration ; 4 | ; ; 5 | ; This file configures a pyprofibus instance. ; 6 | ; ; 7 | ; ----------------------------------------------- ; 8 | 9 | 10 | ; General settings 11 | [PROFIBUS] 12 | 13 | ; Enable/disable debug mode. 14 | ; 0 -> no debugging. 15 | ; 1 -> DP debugging. 16 | ; 2 -> DP and PHY debugging. 17 | debug=1 18 | 19 | 20 | ; PHY protocol layer configuration 21 | [PHY] 22 | 23 | ; The PHY layer driver type. 24 | ;type=serial 25 | ;type=fpga 26 | type=dummy_slave 27 | 28 | ; Only for type=serial: 29 | ; The PHY device name/path. 30 | ; Can be a device like /dev/ttyS0 or /dev/ttyAMA0 31 | dev=/dev/ttyS0 32 | 33 | ; Only for type=serial: 34 | ; Serial line flow control and handshaking 35 | rtscts=False 36 | dsrdtr=False 37 | 38 | ; Only for type=fpga: 39 | ; SPI bus (to PHY FPGA) configuration. 40 | spiBus=0 41 | spiCS=0 42 | spiSpeedHz=2500000 43 | 44 | ; The Profibus on-wire baud rate. 45 | ;baud=9600 46 | baud=19200 47 | ;baud=45450 48 | ;baud=93750 49 | ;baud=187500 50 | ;baud=500000 51 | ;baud=1500000 52 | ;baud=3000000 53 | ;baud=6000000 54 | ;baud=12000000 55 | 56 | 57 | ; FDL protocol layer configuration 58 | [FDL] 59 | 60 | 61 | ; DP protocol layer configuration 62 | [DP] 63 | 64 | ; The master device class. Either 1 or 2. 65 | master_class=1 66 | 67 | ; The Profibus address of this device. 68 | master_addr=2 69 | 70 | 71 | ; --- 72 | ; Slave configurations 73 | ; Add as many [SLAVE_xxx] sections as needed. 74 | ; --- 75 | 76 | ; First slave configuration 77 | [SLAVE_0] 78 | 79 | ; Optional slave name. Will be stored in slaveConf.name and slaveDesc.name. 80 | ; pyprofibus does not use the name internally. 81 | name=first 82 | 83 | ; This slave's Profibus address 84 | addr=8 85 | 86 | ; The path to the GSD file. 87 | gsd=../misc/dummy_modular.gsd 88 | 89 | ; Boolean: Sync mode enabled/available? 90 | sync_mode=1 91 | 92 | ; Boolean: Freeze mode enabled/available? 93 | freeze_mode=1 94 | 95 | ; 8 bit integer specifying the Profibus group ident mask. 96 | group_mask=1 97 | 98 | ; This slave's watchdog timeout, in milliseconds. 99 | watchdog_ms=300 100 | 101 | ; Module configuration. 102 | ; For each module plugged into the slave, add a module_xxx 103 | ; entry with the name of the module. 104 | ; The module name must match the name from the GSD file (approximately). 105 | ; The modules are used in the order of the index number. 106 | module_0=dummy output module 107 | module_1=dummy output module 108 | module_2=dummy input module 109 | 110 | ; The number of output bytes this slave transmits to the 111 | ; master in Data_Exchange. 112 | ; This usually depends on the modules plugged into the slave. 113 | output_size=0 114 | 115 | ; The number of input bytes this slave expects to receive 116 | ; in Data_Exchange. 117 | ; This usually depends on the modules plugged into the slave. 118 | input_size=2 119 | 120 | ; Request and interpret a slave diagnosis every n Data_Exchange telegrams. 121 | ; n defaults to 0, which means: Never periodically request diagnosis. 122 | ; If periodic diagnosis is switched off, then diagnostic information will only be requested on faults. 123 | ; Note that input-only slaves (output_size=0) probably need a non-zero diag_period. 124 | diag_period=16 125 | -------------------------------------------------------------------------------- /examples/example_dummy_inputonly.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Simple pyprofibus dummy example using dummy PHY. 4 | # This example can be run without any PB hardware. 5 | # 6 | 7 | import sys 8 | sys.path.insert(0, "..") 9 | import pyprofibus 10 | import time 11 | 12 | def main(confdir=".", watchdog=None): 13 | master = None 14 | try: 15 | # Parse the config file. 16 | config = pyprofibus.PbConf.fromFile(confdir + "/example_dummy_inputonly.conf") 17 | 18 | # Create a DP master. 19 | master = config.makeDPM() 20 | 21 | # Create the slave descriptions. 22 | outData = {} 23 | for slaveConf in config.slaveConfs: 24 | slaveDesc = slaveConf.makeDpSlaveDesc() 25 | 26 | # Set User_Prm_Data 27 | dp1PrmMask = bytearray((pyprofibus.dp.DpTelegram_SetPrm_Req.DPV1PRM0_FAILSAFE, 28 | pyprofibus.dp.DpTelegram_SetPrm_Req.DPV1PRM1_REDCFG, 29 | 0x00)) 30 | dp1PrmSet = bytearray((pyprofibus.dp.DpTelegram_SetPrm_Req.DPV1PRM0_FAILSAFE, 31 | pyprofibus.dp.DpTelegram_SetPrm_Req.DPV1PRM1_REDCFG, 32 | 0x00)) 33 | slaveDesc.setUserPrmData(slaveConf.gsd.getUserPrmData(dp1PrmMask=dp1PrmMask, 34 | dp1PrmSet=dp1PrmSet)) 35 | 36 | 37 | # Register the slave at the DPM 38 | master.addSlave(slaveDesc) 39 | 40 | # Set initial output data. 41 | outData[slaveDesc.name] = bytearray((0x42, 0x24)) 42 | 43 | # Initialize the DPM 44 | master.initialize() 45 | 46 | # Run the slave state machine. 47 | while True: 48 | # Write the output data. 49 | for slaveDesc in master.getSlaveList(): 50 | slaveDesc.setMasterOutData(outData[slaveDesc.name]) 51 | 52 | # Run slave state machines. 53 | handledSlaveDesc = master.run() 54 | 55 | # Get the in-data (receive) 56 | if handledSlaveDesc: 57 | inData = handledSlaveDesc.getMasterInData() 58 | # This slave will not send any data. It's input-only (see config). 59 | assert inData is None 60 | 61 | # Feed the system watchdog, if it is available. 62 | if watchdog is not None: 63 | watchdog() 64 | # Slow down main loop. Just for debugging. 65 | time.sleep(0.01) 66 | 67 | except pyprofibus.ProfibusError as e: 68 | print("Terminating: %s" % str(e)) 69 | return 1 70 | finally: 71 | if master: 72 | master.destroy() 73 | return 0 74 | 75 | if __name__ == "__main__": 76 | import sys 77 | sys.exit(main()) 78 | -------------------------------------------------------------------------------- /examples/example_dummy_oneslave.conf: -------------------------------------------------------------------------------- 1 | ; ----------------------------------------------- ; 2 | ; ; 3 | ; PROFIBUS configuration ; 4 | ; ; 5 | ; This file configures a pyprofibus instance. ; 6 | ; ; 7 | ; ----------------------------------------------- ; 8 | 9 | 10 | ; General settings 11 | [PROFIBUS] 12 | 13 | ; Enable/disable debug mode. 14 | ; 0 -> no debugging. 15 | ; 1 -> DP debugging. 16 | ; 2 -> DP and PHY debugging. 17 | debug=1 18 | 19 | 20 | ; PHY protocol layer configuration 21 | [PHY] 22 | 23 | ; The PHY layer driver type. 24 | ;type=serial 25 | ;type=fpga 26 | type=dummy_slave 27 | 28 | ; Only for type=serial: 29 | ; The PHY device name/path. 30 | ; Can be a device like /dev/ttyS0 or /dev/ttyAMA0 31 | dev=/dev/ttyS0 32 | 33 | ; Only for type=serial: 34 | ; Serial line flow control and handshaking 35 | rtscts=False 36 | dsrdtr=False 37 | 38 | ; Only for type=fpga: 39 | ; SPI bus (to PHY FPGA) configuration. 40 | spiBus=0 41 | spiCS=0 42 | spiSpeedHz=2500000 43 | 44 | ; The Profibus on-wire baud rate. 45 | ;baud=9600 46 | baud=19200 47 | ;baud=45450 48 | ;baud=93750 49 | ;baud=187500 50 | ;baud=500000 51 | ;baud=1500000 52 | ;baud=3000000 53 | ;baud=6000000 54 | ;baud=12000000 55 | 56 | 57 | ; FDL protocol layer configuration 58 | [FDL] 59 | 60 | 61 | ; DP protocol layer configuration 62 | [DP] 63 | 64 | ; The master device class. Either 1 or 2. 65 | master_class=1 66 | 67 | ; The Profibus address of this device. 68 | master_addr=2 69 | 70 | 71 | ; --- 72 | ; Slave configurations 73 | ; Add as many [SLAVE_xxx] sections as needed. 74 | ; --- 75 | 76 | ; First slave configuration 77 | [SLAVE_0] 78 | 79 | ; Optional slave name. Will be stored in slaveConf.name and slaveDesc.name. 80 | ; pyprofibus does not use the name internally. 81 | name=first 82 | 83 | ; This slave's Profibus address 84 | addr=8 85 | 86 | ; The path to the GSD file. 87 | gsd=../misc/dummy_modular.gsd 88 | 89 | ; Boolean: Sync mode enabled/available? 90 | sync_mode=1 91 | 92 | ; Boolean: Freeze mode enabled/available? 93 | freeze_mode=1 94 | 95 | ; 8 bit integer specifying the Profibus group ident mask. 96 | group_mask=1 97 | 98 | ; This slave's watchdog timeout, in milliseconds. 99 | watchdog_ms=300 100 | 101 | ; Module configuration. 102 | ; For each module plugged into the slave, add a module_xxx 103 | ; entry with the name of the module. 104 | ; The module name must match the name from the GSD file (approximately). 105 | ; The modules are used in the order of the index number. 106 | module_0=dummy output module 107 | module_1=dummy output module 108 | module_2=dummy input module 109 | 110 | ; The number of output bytes this slave transmits to the 111 | ; master in Data_Exchange. 112 | ; This usually depends on the modules plugged into the slave. 113 | output_size=2 114 | 115 | ; The number of input bytes this slave expects to receive 116 | ; in Data_Exchange. 117 | ; This usually depends on the modules plugged into the slave. 118 | input_size=2 119 | 120 | ; Request and interpret a slave diagnosis every n Data_Exchange telegrams. 121 | ; n defaults to 0, which means: Never periodically request diagnosis. 122 | ; If periodic diagnosis is switched off, then diagnostic information will only be requested on faults. 123 | ; Note that input-only slaves (output_size=0) probably need a non-zero diag_period. 124 | diag_period=0 125 | -------------------------------------------------------------------------------- /examples/example_dummy_oneslave.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Simple pyprofibus dummy example using dummy PHY. 4 | # This example can be run without any PB hardware. 5 | # 6 | 7 | import sys 8 | sys.path.insert(0, "..") 9 | import pyprofibus 10 | 11 | def main(confdir=".", watchdog=None): 12 | master = None 13 | try: 14 | # Parse the config file. 15 | config = pyprofibus.PbConf.fromFile(confdir + "/example_dummy_oneslave.conf") 16 | 17 | # Create a DP master. 18 | master = config.makeDPM() 19 | 20 | # Create the slave descriptions. 21 | outData = {} 22 | for slaveConf in config.slaveConfs: 23 | slaveDesc = slaveConf.makeDpSlaveDesc() 24 | 25 | # Set User_Prm_Data 26 | dp1PrmMask = bytearray((pyprofibus.dp.DpTelegram_SetPrm_Req.DPV1PRM0_FAILSAFE, 27 | pyprofibus.dp.DpTelegram_SetPrm_Req.DPV1PRM1_REDCFG, 28 | 0x00)) 29 | dp1PrmSet = bytearray((pyprofibus.dp.DpTelegram_SetPrm_Req.DPV1PRM0_FAILSAFE, 30 | pyprofibus.dp.DpTelegram_SetPrm_Req.DPV1PRM1_REDCFG, 31 | 0x00)) 32 | slaveDesc.setUserPrmData(slaveConf.gsd.getUserPrmData(dp1PrmMask=dp1PrmMask, 33 | dp1PrmSet=dp1PrmSet)) 34 | 35 | 36 | # Register the slave at the DPM 37 | master.addSlave(slaveDesc) 38 | 39 | # Set initial output data. 40 | outData[slaveDesc.name] = bytearray((0x42, 0x24)) 41 | 42 | # Initialize the DPM 43 | master.initialize() 44 | 45 | # Run the slave state machine. 46 | while True: 47 | # Write the output data. 48 | for slaveDesc in master.getSlaveList(): 49 | slaveDesc.setMasterOutData(outData[slaveDesc.name]) 50 | 51 | # Run slave state machines. 52 | handledSlaveDesc = master.run() 53 | 54 | # Get the in-data (receive) 55 | if handledSlaveDesc: 56 | inData = handledSlaveDesc.getMasterInData() 57 | if inData is not None: 58 | # In our example the output data shall be the inverted input. 59 | outData["first"][0] = inData[1] 60 | 61 | # Feed the system watchdog, if it is available. 62 | if watchdog is not None: 63 | watchdog() 64 | 65 | except pyprofibus.ProfibusError as e: 66 | print("Terminating: %s" % str(e)) 67 | return 1 68 | finally: 69 | if master: 70 | master.destroy() 71 | return 0 72 | 73 | if __name__ == "__main__": 74 | import sys 75 | sys.exit(main()) 76 | -------------------------------------------------------------------------------- /examples/example_dummy_twoslaves.conf: -------------------------------------------------------------------------------- 1 | ; ----------------------------------------------- ; 2 | ; ; 3 | ; PROFIBUS configuration ; 4 | ; ; 5 | ; This file configures a pyprofibus instance. ; 6 | ; ; 7 | ; ----------------------------------------------- ; 8 | 9 | 10 | ; General settings 11 | [PROFIBUS] 12 | 13 | ; Enable/disable debug mode. 14 | ; 0 -> no debugging. 15 | ; 1 -> DP debugging. 16 | ; 2 -> DP and PHY debugging. 17 | debug=1 18 | 19 | 20 | ; PHY protocol layer configuration 21 | [PHY] 22 | 23 | ; The PHY layer driver type. 24 | ;type=serial 25 | ;type=fpga 26 | type=dummy_slave 27 | 28 | ; Only for type=serial: 29 | ; The PHY device name/path. 30 | ; Can be a device like /dev/ttyS0 or /dev/ttyAMA0 31 | dev=/dev/ttyS0 32 | 33 | ; Only for type=serial: 34 | ; Serial line flow control and handshaking 35 | rtscts=False 36 | dsrdtr=False 37 | 38 | ; Only for type=fpga: 39 | ; SPI bus (to PHY FPGA) configuration. 40 | spiBus=0 41 | spiCS=0 42 | spiSpeedHz=2500000 43 | 44 | ; The Profibus on-wire baud rate. 45 | ;baud=9600 46 | baud=19200 47 | ;baud=45450 48 | ;baud=93750 49 | ;baud=187500 50 | ;baud=500000 51 | ;baud=1500000 52 | ;baud=3000000 53 | ;baud=6000000 54 | ;baud=12000000 55 | 56 | 57 | ; FDL protocol layer configuration 58 | [FDL] 59 | 60 | 61 | ; DP protocol layer configuration 62 | [DP] 63 | 64 | ; The master device class. Either 1 or 2. 65 | master_class=1 66 | 67 | ; The Profibus address of this device. 68 | master_addr=2 69 | 70 | 71 | ; --- 72 | ; Slave configurations 73 | ; Add as many [SLAVE_xxx] sections as needed. 74 | ; --- 75 | 76 | ; First slave configuration 77 | [SLAVE_0] 78 | 79 | ; Optional slave name. Will be stored in slaveConf.name and slaveDesc.name. 80 | ; pyprofibus does not use the name internally. 81 | name=first 82 | 83 | ; This slave's Profibus address 84 | addr=8 85 | 86 | ; The path to the GSD file. 87 | gsd=../misc/dummy_modular.gsd 88 | 89 | ; Boolean: Sync mode enabled/available? 90 | sync_mode=1 91 | 92 | ; Boolean: Freeze mode enabled/available? 93 | freeze_mode=1 94 | 95 | ; 8 bit integer specifying the Profibus group ident mask. 96 | group_mask=1 97 | 98 | ; This slave's watchdog timeout, in milliseconds. 99 | watchdog_ms=300 100 | 101 | ; Module configuration. 102 | ; For each module plugged into the slave, add a module_xxx 103 | ; entry with the name of the module. 104 | ; The module name must match the name from the GSD file (approximately). 105 | ; The modules are used in the order of the index number. 106 | module_0=dummy output module 107 | module_1=dummy output module 108 | module_2=dummy input module 109 | 110 | ; The number of output bytes this slave transmits to the 111 | ; master in Data_Exchange. 112 | ; This usually depends on the modules plugged into the slave. 113 | output_size=2 114 | 115 | ; The number of input bytes this slave expects to receive 116 | ; in Data_Exchange. 117 | ; This usually depends on the modules plugged into the slave. 118 | input_size=2 119 | 120 | ; Request and interpret a slave diagnosis every n Data_Exchange telegrams. 121 | ; n defaults to 0, which means: Never periodically request diagnosis. 122 | ; If periodic diagnosis is switched off, then diagnostic information will only be requested on faults. 123 | ; Note that input-only slaves (output_size=0) probably need a non-zero diag_period. 124 | diag_period=0 125 | 126 | 127 | ; Second slave configuration 128 | [SLAVE_1] 129 | 130 | ; Optional slave name. Will be stored in slaveConf.name and slaveDesc.name. 131 | ; pyprofibus does not use the name internally. 132 | name=second 133 | 134 | ; This slave's Profibus address 135 | addr=42 136 | 137 | ; The path to the GSD file. 138 | gsd=../misc/dummy_modular.gsd 139 | 140 | ; Boolean: Sync mode enabled/available? 141 | sync_mode=1 142 | 143 | ; Boolean: Freeze mode enabled/available? 144 | freeze_mode=1 145 | 146 | ; 8 bit integer specifying the Profibus group ident mask. 147 | group_mask=1 148 | 149 | ; This slave's watchdog timeout, in milliseconds. 150 | watchdog_ms=300 151 | 152 | ; Module configuration. 153 | ; For each module plugged into the slave, add a module_xxx 154 | ; entry with the name of the module. 155 | ; The module name must match the name from the GSD file (approximately). 156 | ; The modules are used in the order of the index number. 157 | module_0=dummy output module 158 | module_1=dummy output module 159 | module_2=dummy input module 160 | 161 | ; The number of output bytes this slave transmits to the 162 | ; master in Data_Exchange. 163 | ; This usually depends on the modules plugged into the slave. 164 | output_size=2 165 | 166 | ; The number of input bytes this slave expects to receive 167 | ; in Data_Exchange. 168 | ; This usually depends on the modules plugged into the slave. 169 | input_size=2 170 | 171 | ; Request and interpret a slave diagnosis every n Data_Exchange telegrams. 172 | ; n defaults to 0, which means: Never periodically request diagnosis. 173 | ; If periodic diagnosis is switched off, then diagnostic information will only be requested on faults. 174 | ; Note that input-only slaves (output_size=0) probably need a non-zero diag_period. 175 | diag_period=0 176 | -------------------------------------------------------------------------------- /examples/example_dummy_twoslaves.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Simple pyprofibus dummy example using dummy PHY. 4 | # This example can be run without any PB hardware. 5 | # 6 | 7 | import sys 8 | sys.path.insert(0, "..") 9 | import pyprofibus 10 | 11 | def main(confdir=".", watchdog=None): 12 | master = None 13 | try: 14 | # Parse the config file. 15 | config = pyprofibus.PbConf.fromFile(confdir + "/example_dummy_twoslaves.conf") 16 | 17 | # Create a DP master. 18 | master = config.makeDPM() 19 | 20 | # Create the slave descriptions. 21 | outData = {} 22 | for slaveConf in config.slaveConfs: 23 | slaveDesc = slaveConf.makeDpSlaveDesc() 24 | 25 | # Set User_Prm_Data 26 | dp1PrmMask = bytearray((pyprofibus.dp.DpTelegram_SetPrm_Req.DPV1PRM0_FAILSAFE, 27 | pyprofibus.dp.DpTelegram_SetPrm_Req.DPV1PRM1_REDCFG, 28 | 0x00)) 29 | dp1PrmSet = bytearray((pyprofibus.dp.DpTelegram_SetPrm_Req.DPV1PRM0_FAILSAFE, 30 | pyprofibus.dp.DpTelegram_SetPrm_Req.DPV1PRM1_REDCFG, 31 | 0x00)) 32 | slaveDesc.setUserPrmData(slaveConf.gsd.getUserPrmData(dp1PrmMask=dp1PrmMask, 33 | dp1PrmSet=dp1PrmSet)) 34 | 35 | 36 | # Register the slave at the DPM 37 | master.addSlave(slaveDesc) 38 | 39 | # Set initial output data. 40 | outData[slaveDesc.name] = bytearray((0x42, 0x24)) 41 | 42 | # Initialize the DPM 43 | master.initialize() 44 | 45 | # Run the slave state machine. 46 | while True: 47 | # Write the output data. 48 | for slaveDesc in master.getSlaveList(): 49 | slaveDesc.setMasterOutData(outData[slaveDesc.name]) 50 | 51 | # Run slave state machines. 52 | handledSlaveDesc = master.run() 53 | 54 | # Get the in-data (receive) 55 | if handledSlaveDesc: 56 | inData = handledSlaveDesc.getMasterInData() 57 | if inData is not None: 58 | # In our example the output data shall be the inverted input. 59 | outData[handledSlaveDesc.name][0] = inData[1] 60 | outData[handledSlaveDesc.name][1] = inData[0] 61 | 62 | # Feed the system watchdog, if it is available. 63 | if watchdog is not None: 64 | watchdog() 65 | 66 | except pyprofibus.ProfibusError as e: 67 | print("Terminating: %s" % str(e)) 68 | return 1 69 | finally: 70 | if master: 71 | master.destroy() 72 | return 0 73 | 74 | if __name__ == "__main__": 75 | import sys 76 | sys.exit(main()) 77 | -------------------------------------------------------------------------------- /examples/example_et200s.conf: -------------------------------------------------------------------------------- 1 | ; ----------------------------------------------- ; 2 | ; ; 3 | ; PROFIBUS configuration ; 4 | ; ; 5 | ; This file configures a pyprofibus instance. ; 6 | ; ; 7 | ; ----------------------------------------------- ; 8 | 9 | 10 | ; General settings 11 | [PROFIBUS] 12 | 13 | ; Enable/disable debug mode. 14 | ; 0 -> no debugging. 15 | ; 1 -> DP debugging. 16 | ; 2 -> DP and PHY debugging. 17 | debug=1 18 | 19 | 20 | ; PHY protocol layer configuration 21 | [PHY] 22 | 23 | ; The PHY layer driver type. 24 | type=serial 25 | ;type=fpga 26 | ;type=dummy_slave 27 | 28 | ; Only for type=serial: 29 | ; The PHY device name/path. 30 | ; Can be a device like /dev/ttyS0 or /dev/ttyAMA0 31 | dev=/dev/ttyS0 32 | 33 | ; Only for type=serial: 34 | ; Serial line flow control and handshaking 35 | rtscts=False 36 | dsrdtr=False 37 | 38 | ; Only for type=fpga: 39 | ; SPI bus (to PHY FPGA) configuration. 40 | spiBus=0 41 | spiCS=0 42 | spiSpeedHz=2500000 43 | 44 | ; The Profibus on-wire baud rate. 45 | ;baud=9600 46 | baud=19200 47 | ;baud=45450 48 | ;baud=93750 49 | ;baud=187500 50 | ;baud=500000 51 | ;baud=1500000 52 | ;baud=3000000 53 | ;baud=6000000 54 | ;baud=12000000 55 | 56 | 57 | ; FDL protocol layer configuration 58 | [FDL] 59 | 60 | 61 | ; DP protocol layer configuration 62 | [DP] 63 | 64 | ; The master device class. Either 1 or 2. 65 | master_class=1 66 | 67 | ; The Profibus address of this device. 68 | master_addr=2 69 | 70 | 71 | ; --- 72 | ; Slave configurations 73 | ; Add as many [SLAVE_xxx] sections as needed. 74 | ; --- 75 | 76 | ; First slave configuration 77 | [SLAVE_0] 78 | 79 | ; Optional slave name. Will be stored in slaveConf.name and slaveDesc.name. 80 | ; pyprofibus does not use the name internally. 81 | name=et200s 82 | 83 | ; This slave's Profibus address 84 | addr=8 85 | 86 | ; The path to the GSD file. 87 | gsd=si03806a.gsd 88 | 89 | ; Boolean: Sync mode enabled/available? 90 | sync_mode=1 91 | 92 | ; Boolean: Freeze mode enabled/available? 93 | freeze_mode=1 94 | 95 | ; 8 bit integer specifying the Profibus group ident mask. 96 | group_mask=1 97 | 98 | ; This slave's watchdog timeout, in milliseconds. 99 | watchdog_ms=300 100 | 101 | ; Module configuration. 102 | ; For each module plugged into the slave, add a module_xxx 103 | ; entry with the name of the module. 104 | ; The module name must match the name from the GSD file (approximately). 105 | ; The modules are used in the order of the index number. 106 | module_0=6ES7 138-4CA01-0AA0 PM-E DC24V 107 | module_1=6ES7 132-4BB30-0AA0 2DO DC24V 108 | module_2=6ES7 132-4BB30-0AA0 2DO DC24V 109 | module_3=6ES7 131-4BD01-0AA0 4DI DC24V 110 | 111 | ; The number of output bytes this slave transmits to the 112 | ; master in Data_Exchange. 113 | ; This usually depends on the modules plugged into the slave. 114 | output_size=1 115 | 116 | ; The number of input bytes this slave expects to receive 117 | ; in Data_Exchange. 118 | ; This usually depends on the modules plugged into the slave. 119 | input_size=2 120 | 121 | ; Request and interpret a slave diagnosis every n Data_Exchange telegrams. 122 | ; n defaults to 0, which means: Never periodically request diagnosis. 123 | ; If periodic diagnosis is switched off, then diagnostic information will only be requested on faults. 124 | ; Note that input-only slaves (output_size=0) probably need a non-zero diag_period. 125 | diag_period=0 126 | -------------------------------------------------------------------------------- /examples/example_et200s.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Simple pyprofibus example 4 | # 5 | # This example initializes an ET-200S slave, reads input 6 | # data and writes the data back to the module. 7 | # 8 | # The hardware configuration is as follows: 9 | # 10 | # v--------------v----------v----------v----------v----------v 11 | # | IM 151-1 | PM-E | 2 DO | 2 DO | 4 DI | 12 | # | STANDARD | DC24V | DC24V/2A | DC24V/2A | DC24V | 13 | # | | | | | | 14 | # | | | | | | 15 | # | ET 200S | | | | | 16 | # | | | | | | 17 | # | | | | | | 18 | # | 6ES7 | 6ES7 | 6ES7 | 6ES7 | 6ES7 | 19 | # | 151- | 138- | 132- | 132- | 131- | 20 | # | 1AA04- | 4CA01- | 4BB30- | 4BB30- | 4BD01- | 21 | # | 0AB0 | 0AA0 | 0AA0 | 0AA0 | 0AA0 | 22 | # ^--------------^----------^----------^----------^----------^ 23 | # 24 | 25 | import sys 26 | sys.path.insert(0, "..") 27 | import pyprofibus 28 | 29 | def main(confdir=".", watchdog=None): 30 | master = None 31 | try: 32 | # Parse the config file. 33 | config = pyprofibus.PbConf.fromFile(confdir + "/example_et200s.conf") 34 | 35 | # Create a DP master. 36 | master = config.makeDPM() 37 | 38 | # Create the slave descriptions. 39 | outData = {} 40 | for slaveConf in config.slaveConfs: 41 | slaveDesc = slaveConf.makeDpSlaveDesc() 42 | 43 | # Set User_Prm_Data 44 | dp1PrmMask = bytearray((pyprofibus.dp.DpTelegram_SetPrm_Req.DPV1PRM0_FAILSAFE, 45 | pyprofibus.dp.DpTelegram_SetPrm_Req.DPV1PRM1_REDCFG, 46 | 0x00)) 47 | dp1PrmSet = bytearray((pyprofibus.dp.DpTelegram_SetPrm_Req.DPV1PRM0_FAILSAFE, 48 | pyprofibus.dp.DpTelegram_SetPrm_Req.DPV1PRM1_REDCFG, 49 | 0x00)) 50 | slaveDesc.setUserPrmData(slaveConf.gsd.getUserPrmData(dp1PrmMask=dp1PrmMask, 51 | dp1PrmSet=dp1PrmSet)) 52 | 53 | # Register the ET-200S slave at the DPM 54 | master.addSlave(slaveDesc) 55 | 56 | # Set initial output data. 57 | outData[slaveDesc.name] = bytearray((0x00, 0x00)) 58 | 59 | # Initialize the DPM 60 | master.initialize() 61 | 62 | # Cyclically run Data_Exchange. 63 | while True: 64 | # Write the output data. 65 | for slaveDesc in master.getSlaveList(): 66 | slaveDesc.setMasterOutData(outData[slaveDesc.name]) 67 | 68 | # Run slave state machines. 69 | handledSlaveDesc = master.run() 70 | 71 | # Get the in-data (receive) 72 | if handledSlaveDesc: 73 | inData = handledSlaveDesc.getMasterInData() 74 | if inData is not None: 75 | # In our example the output data shall be a mirror of the input. 76 | outData[handledSlaveDesc.name][0] = inData[0] & 3 77 | outData[handledSlaveDesc.name][1] = (inData[0] >> 2) & 3 78 | 79 | # Feed the system watchdog, if it is available. 80 | if watchdog is not None: 81 | watchdog() 82 | 83 | except pyprofibus.ProfibusError as e: 84 | print("Terminating: %s" % str(e)) 85 | return 1 86 | finally: 87 | if master: 88 | master.destroy() 89 | return 0 90 | 91 | if __name__ == "__main__": 92 | import sys 93 | sys.exit(main()) 94 | -------------------------------------------------------------------------------- /examples/example_s7_315_2dp.conf: -------------------------------------------------------------------------------- 1 | ; ----------------------------------------------- ; 2 | ; ; 3 | ; PROFIBUS configuration ; 4 | ; ; 5 | ; This file configures a pyprofibus instance. ; 6 | ; ; 7 | ; ----------------------------------------------- ; 8 | 9 | 10 | ; General settings 11 | [PROFIBUS] 12 | 13 | ; Enable/disable debug mode. 14 | ; 0 -> no debugging. 15 | ; 1 -> DP debugging. 16 | ; 2 -> DP and PHY debugging. 17 | debug=1 18 | 19 | 20 | ; PHY protocol layer configuration 21 | [PHY] 22 | 23 | ; The PHY layer driver type. 24 | type=serial 25 | ;type=fpga 26 | ;type=dummy_slave 27 | 28 | ; Only for type=serial: 29 | ; The PHY device name/path. 30 | ; Can be a device like /dev/ttyS0 or /dev/ttyAMA0 31 | dev=/dev/ttyS0 32 | 33 | ; Only for type=serial: 34 | ; Serial line flow control and handshaking 35 | rtscts=False 36 | dsrdtr=False 37 | 38 | ; Only for type=fpga: 39 | ; SPI bus (to PHY FPGA) configuration. 40 | spiBus=0 41 | spiCS=0 42 | spiSpeedHz=2500000 43 | 44 | ; The Profibus on-wire baud rate. 45 | ;baud=9600 46 | baud=19200 47 | ;baud=45450 48 | ;baud=93750 49 | ;baud=187500 50 | ;baud=500000 51 | ;baud=1500000 52 | ;baud=3000000 53 | ;baud=6000000 54 | ;baud=12000000 55 | 56 | 57 | ; FDL protocol layer configuration 58 | [FDL] 59 | 60 | 61 | ; DP protocol layer configuration 62 | [DP] 63 | 64 | ; The master device class. Either 1 or 2. 65 | master_class=1 66 | 67 | ; The Profibus address of this device. 68 | master_addr=2 69 | 70 | 71 | ; --- 72 | ; Slave configurations 73 | ; Add as many [SLAVE_xxx] sections as needed. 74 | ; --- 75 | 76 | ; First slave configuration 77 | [SLAVE_0] 78 | 79 | ; Optional slave name. Will be stored in slaveConf.name and slaveDesc.name. 80 | ; pyprofibus does not use the name internally. 81 | name=s7_315_2dp 82 | 83 | ; This slave's Profibus address 84 | addr=9 85 | 86 | ; The path to the GSD file. 87 | gsd=sie3802f.gsd 88 | 89 | ; Boolean: Sync mode enabled/available? 90 | sync_mode=0 91 | 92 | ; Boolean: Freeze mode enabled/available? 93 | freeze_mode=0 94 | 95 | ; 8 bit integer specifying the Profibus group ident mask. 96 | group_mask=1 97 | 98 | ; This slave's watchdog timeout, in milliseconds. 99 | watchdog_ms=0 100 | 101 | ; Module configuration. 102 | ; For each module plugged into the slave, add a module_xxx 103 | ; entry with the name of the module. 104 | ; The module name must match the name from the GSD file (approximately). 105 | ; The modules are used in the order of the index number. 106 | module_0=Master_O Slave_I 1 by unit 107 | module_1=Master_I Slave_O 1 by unit 108 | 109 | ; The number of output bytes this slave transmits to the 110 | ; master in Data_Exchange. 111 | ; This usually depends on the modules plugged into the slave. 112 | output_size=1 113 | 114 | ; The number of input bytes this slave expects to receive 115 | ; in Data_Exchange. 116 | ; This usually depends on the modules plugged into the slave. 117 | input_size=1 118 | 119 | ; Request and interpret a slave diagnosis every n Data_Exchange telegrams. 120 | ; n defaults to 0, which means: Never periodically request diagnosis. 121 | ; If periodic diagnosis is switched off, then diagnostic information will only be requested on faults. 122 | ; Note that input-only slaves (output_size=0) probably need a non-zero diag_period. 123 | diag_period=0 124 | -------------------------------------------------------------------------------- /examples/example_s7_315_2dp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Simple pyprofibus example 4 | # 5 | # This example initializes an S7-315-2DP configured as slave, 6 | # reads its input data and writes the data back to the module. 7 | # 8 | 9 | import sys 10 | sys.path.insert(0, "..") 11 | import pyprofibus 12 | 13 | def main(confdir=".", watchdog=None): 14 | master = None 15 | try: 16 | # Parse the config file. 17 | config = pyprofibus.PbConf.fromFile(confdir + "/example_s7_315_2dp.conf") 18 | 19 | # Create a DP master. 20 | master = config.makeDPM() 21 | 22 | # Create the slave descriptions. 23 | outData = {} 24 | for slaveConf in config.slaveConfs: 25 | slaveDesc = slaveConf.makeDpSlaveDesc() 26 | 27 | # Register the S7-315-2DP slave at the DPM 28 | master.addSlave(slaveDesc) 29 | 30 | # Set initial output data. 31 | outData[slaveDesc.slaveAddr] = bytearray((0x00, )) 32 | 33 | # Initialize the DPM 34 | master.initialize() 35 | 36 | # Cyclically run Data_Exchange. 37 | while True: 38 | # Write the output data. 39 | for slaveDesc in master.getSlaveList(): 40 | slaveDesc.setMasterOutData(outData[slaveDesc.slaveAddr]) 41 | 42 | # Run slave state machines. 43 | handledSlaveDesc = master.run() 44 | 45 | # Get the in-data (receive) and set it as out-data (transmit). 46 | if handledSlaveDesc: 47 | inData = handledSlaveDesc.getMasterInData() 48 | if inData is not None: 49 | # In our example the output data shall be a mirror of the input. 50 | outData[handledSlaveDesc.slaveAddr] = inData 51 | 52 | # Feed the system watchdog, if it is available. 53 | if watchdog is not None: 54 | watchdog() 55 | 56 | except pyprofibus.ProfibusError as e: 57 | print("Terminating: %s" % str(e)) 58 | return 1 59 | finally: 60 | if master: 61 | master.destroy() 62 | return 0 63 | 64 | if __name__ == "__main__": 65 | import sys 66 | sys.exit(main()) 67 | -------------------------------------------------------------------------------- /examples/linuxcnc/dummy_phy/.gitignore: -------------------------------------------------------------------------------- 1 | rs274ngc.var* 2 | emc.var* 3 | -------------------------------------------------------------------------------- /examples/linuxcnc/dummy_phy/dummy.gsd: -------------------------------------------------------------------------------- 1 | ; 2 | ; pyprofibus - Dummy GSD file 3 | ; 4 | 5 | #Profibus_DP 6 | 7 | GSD_Revision=1 8 | Slave_Family=3@Digital@24V 9 | Vendor_Name="PYPROFIBUS" 10 | Model_Name="PYPROFIBUS DUMMY" 11 | OrderNumber="42-42-42" 12 | Revision="42" 13 | Hardware_Release="42" 14 | Software_Release="42" 15 | Ident_Number=0x4224 16 | Protocol_Ident=0 17 | Station_Type=0 18 | Fail_Safe=1 19 | DPV1_Slave=1 20 | 21 | 22 | PrmText=1 23 | Text(0)="disabled" 24 | Text(1)="enabled" 25 | EndPrmText 26 | 27 | ExtUserPrmData=1 "dummy feature 1" 28 | Bit(2) 0 0-1 29 | Prm_Text_Ref=1 30 | EndExtUserPrmData 31 | 32 | ExtUserPrmData=2 "dummy feature 2" 33 | Bit(3) 0 0-1 34 | Prm_Text_Ref=1 35 | EndExtUserPrmData 36 | 37 | 38 | Auto_Baud_supp=1 39 | 9.6_supp=1 40 | 19.2_supp=1 41 | 45.45_supp=1 42 | 93.75_supp=1 43 | 187.5_supp=1 44 | 500_supp=1 45 | 1.5M_supp=1 46 | 3M_supp=1 47 | 6M_supp=1 48 | 12M_supp=1 49 | MaxTsdr_9.6=60 50 | MaxTsdr_19.2=60 51 | MaxTsdr_45.45=250 52 | MaxTsdr_93.75=60 53 | MaxTsdr_187.5=60 54 | MaxTsdr_500=100 55 | MaxTsdr_1.5M=150 56 | MaxTsdr_3M=250 57 | MaxTsdr_6M=450 58 | MaxTsdr_12M=800 59 | 60 | Min_Slave_Intervall=10 61 | 62 | Max_Diag_Data_Len=128 63 | 64 | User_Prm_Data_Len=4 65 | User_Prm_Data = 0x00,0x00,0x00,0x42 66 | Max_User_Prm_Data_Len=16 67 | Ext_User_Prm_Data_Const(0)=0x00,0x00,0x00,0x42 68 | Ext_User_Prm_Data_Ref(3)=1 69 | Ext_User_Prm_Data_Ref(3)=2 70 | 71 | 72 | Modular_Station=1 73 | Modul_Offset=1 74 | Max_Module=32 75 | FixPresetModules=1 76 | Max_Input_Len=249 77 | Max_Output_Len=249 78 | Max_Data_Len=498 79 | 80 | Module="fixed module" 0x00 81 | Preset=1 82 | EndModule 83 | 84 | Module="dummy input module" 0x10 85 | EndModule 86 | 87 | Module="dummy output module" 0x20 88 | EndModule 89 | -------------------------------------------------------------------------------- /examples/linuxcnc/dummy_phy/linuxcnc-demo.hal: -------------------------------------------------------------------------------- 1 | loadrt [KINS]KINEMATICS 2 | loadrt [EMCMOT]EMCMOT base_period_nsec=[EMCMOT]BASE_PERIOD servo_period_nsec=[EMCMOT]SERVO_PERIOD traj_period_nsec=[EMCMOT]SERVO_PERIOD key=[EMCMOT]SHMEM_KEY num_joints=[KINS]JOINTS 3 | 4 | loadrt stepgen step_type=0,0,0 5 | loadrt charge_pump 6 | loadrt comp count=3 7 | loadusr -W hal_manualtoolchange 8 | 9 | # Base thread 10 | addf stepgen.make-pulses base-thread 11 | 12 | # Servo thread 13 | addf stepgen.capture-position servo-thread 14 | addf motion-command-handler servo-thread 15 | addf motion-controller servo-thread 16 | addf stepgen.update-freq servo-thread 17 | addf charge-pump servo-thread 18 | addf comp.0 servo-thread 19 | addf comp.1 servo-thread 20 | addf comp.2 servo-thread 21 | 22 | # Spindle control 23 | net spindle-cw <= spindle.0.forward 24 | net spindle-ccw <= spindle.0.reverse 25 | 26 | # Coolant control 27 | net coolant-mist <= iocontrol.0.coolant-mist 28 | net coolant-flood <= iocontrol.0.coolant-flood 29 | 30 | # heartbeat generator 31 | setp charge-pump.enable 1 32 | net heartbeat <= charge-pump.out-4 33 | 34 | # Emergency stop logic 35 | net estop-out-not <= iocontrol.0.user-enable-out 36 | net estop-in-not => iocontrol.0.emc-enable-in 37 | 38 | # Manual tool change 39 | net tool-change iocontrol.0.tool-change => hal_manualtoolchange.change 40 | net tool-changed iocontrol.0.tool-changed <= hal_manualtoolchange.changed 41 | net tool-number iocontrol.0.tool-prep-number => hal_manualtoolchange.number 42 | net tool-prepare-loopback iocontrol.0.tool-prepare => iocontrol.0.tool-prepared 43 | 44 | # X axis 45 | setp stepgen.0.position-scale [JOINT_0]SCALE 46 | setp stepgen.0.steplen [STEPCONF]STEPLEN 47 | setp stepgen.0.stepspace [STEPCONF]STEPSPACE 48 | setp stepgen.0.dirhold [STEPCONF]DIRHOLD 49 | setp stepgen.0.dirsetup [STEPCONF]DIRSETUP 50 | setp stepgen.0.maxaccel [JOINT_0]STEPGEN_MAXACCEL 51 | net xpos-cmd joint.0.motor-pos-cmd => stepgen.0.position-cmd 52 | net xpos-fb stepgen.0.position-fb => joint.0.motor-pos-fb 53 | net xstep <= stepgen.0.step 54 | net xdir <= stepgen.0.dir 55 | net xenable joint.0.amp-enable-out => stepgen.0.enable 56 | net home-x => joint.0.home-sw-in 57 | net limit-x => joint.0.neg-lim-sw-in 58 | net limit-x => joint.0.pos-lim-sw-in 59 | 60 | # Y axis 61 | setp stepgen.1.position-scale [JOINT_1]SCALE 62 | setp stepgen.1.steplen [STEPCONF]STEPLEN 63 | setp stepgen.1.stepspace [STEPCONF]STEPSPACE 64 | setp stepgen.1.dirhold [STEPCONF]DIRHOLD 65 | setp stepgen.1.dirsetup [STEPCONF]DIRSETUP 66 | setp stepgen.1.maxaccel [JOINT_1]STEPGEN_MAXACCEL 67 | net ypos-cmd joint.1.motor-pos-cmd => stepgen.1.position-cmd 68 | net ypos-fb stepgen.1.position-fb => joint.1.motor-pos-fb 69 | net ystep <= stepgen.1.step 70 | net ydir <= stepgen.1.dir 71 | net yenable joint.1.amp-enable-out => stepgen.1.enable 72 | net home-y => joint.1.home-sw-in 73 | net limit-y => joint.1.neg-lim-sw-in 74 | net limit-y => joint.1.pos-lim-sw-in 75 | 76 | # Z axis 77 | setp stepgen.2.position-scale [JOINT_2]SCALE 78 | setp stepgen.2.steplen [STEPCONF]STEPLEN 79 | setp stepgen.2.stepspace [STEPCONF]STEPSPACE 80 | setp stepgen.2.dirhold [STEPCONF]DIRHOLD 81 | setp stepgen.2.dirsetup [STEPCONF]DIRSETUP 82 | setp stepgen.2.maxaccel [JOINT_2]STEPGEN_MAXACCEL 83 | net zpos-cmd joint.2.motor-pos-cmd => stepgen.2.position-cmd 84 | net zpos-fb stepgen.2.position-fb => joint.2.motor-pos-fb 85 | net zstep <= stepgen.2.step 86 | net zdir <= stepgen.2.dir 87 | net zenable joint.2.amp-enable-out => stepgen.2.enable 88 | net home-z => joint.2.home-sw-in 89 | net limit-z => joint.2.neg-lim-sw-in 90 | net limit-z => joint.2.pos-lim-sw-in 91 | 92 | # Simulate home switches 93 | net xpos-cmd => comp.0.in0 94 | setp comp.0.in1 0 95 | setp comp.0.hyst 0.1 96 | net home-x <= comp.0.out 97 | 98 | net ypos-cmd => comp.1.in0 99 | setp comp.1.in1 0 100 | setp comp.1.hyst 0.1 101 | net home-y <= comp.1.out 102 | 103 | net zpos-cmd => comp.2.in0 104 | setp comp.2.in1 0 105 | setp comp.2.hyst 0.1 106 | net home-z <= comp.2.out 107 | 108 | # Simulate hardware limit switches 109 | sets limit-x 0 110 | sets limit-y 0 111 | sets limit-z 0 112 | -------------------------------------------------------------------------------- /examples/linuxcnc/dummy_phy/linuxcnc-demo.ini: -------------------------------------------------------------------------------- 1 | [EMC] 2 | VERSION = 1.1 3 | MACHINE = linuxcnc-demo 4 | DEBUG = 0 5 | 6 | [DISPLAY] 7 | DISPLAY = axis 8 | POSITION_OFFSET = RELATIVE 9 | POSITION_FEEDBACK = ACTUAL 10 | MAX_FEED_OVERRIDE = 1.2 11 | INTRO_GRAPHIC = emc2.gif 12 | INTRO_TIME = 0 13 | PROGRAM_PREFIX = /tmp 14 | INCREMENTS = 0.1mm 0.05mm 0.01mm 0.005mm 0.001mm 15 | OPEN_FILE = linuxcnc-demo.ngc 16 | 17 | [FILTER] 18 | PROGRAM_EXTENSION = .py Python script 19 | py = python3 20 | 21 | [RS274NGC] 22 | PARAMETER_FILE = emc.var 23 | RS274NGC_STARTUP_CODE = G17 G21 G40 G49 G80 G90 G94 24 | 25 | [EMCMOT] 26 | EMCMOT = motmod 27 | SHMEM_KEY = 111 28 | COMM_TIMEOUT = 1.0 29 | BASE_PERIOD = 20000 30 | SERVO_PERIOD = 1000000 31 | 32 | [TASK] 33 | TASK = milltask 34 | CYCLE_TIME = 0.001 35 | 36 | [HAL] 37 | HALUI = halui 38 | HALFILE = linuxcnc-demo.hal 39 | HALFILE = profibus.hal 40 | 41 | [HALUI] 42 | 43 | [TRAJ] 44 | COORDINATES = XYZ 45 | LINEAR_UNITS = mm 46 | ANGULAR_UNITS = degree 47 | DEFAULT_LINEAR_VELOCITY = 10.0 48 | MAX_LINEAR_VELOCITY = 30.0 49 | NO_FORCE_HOMING = 1 50 | 51 | [EMCIO] 52 | EMCIO = io 53 | CYCLE_TIME = 0.100 54 | TOOL_TABLE = tool.tbl 55 | 56 | [KINS] 57 | KINEMATICS = trivkins coordinates=XYZ 58 | JOINTS = 3 59 | 60 | [AXIS_X] 61 | MIN_LIMIT = -202 62 | MAX_LIMIT = 2 63 | MAX_VELOCITY = 20.0 64 | MAX_ACCELERATION = 100 65 | 66 | [JOINT_0] 67 | TYPE = LINEAR 68 | HOME = -100 69 | MAX_VELOCITY = 20.0 70 | MAX_ACCELERATION = 100 71 | STEPGEN_MAXACCEL = 120 72 | SCALE = 100 73 | FERROR = 20.0 74 | MIN_LIMIT = -202 75 | MAX_LIMIT = 2 76 | HOME_OFFSET = -10 77 | HOME_SEARCH_VEL = -5.0 78 | HOME_LATCH_VEL = -0.5 79 | HOME_IGNORE_LIMITS = NO 80 | HOME_USE_INDEX = NO 81 | HOME_SEQUENCE = 1 82 | 83 | [AXIS_Y] 84 | MIN_LIMIT = -202 85 | MAX_LIMIT = 2 86 | MAX_VELOCITY = 20.0 87 | MAX_ACCELERATION = 100 88 | 89 | [JOINT_1] 90 | TYPE = LINEAR 91 | HOME = -100 92 | MAX_VELOCITY = 20.0 93 | MAX_ACCELERATION = 100 94 | STEPGEN_MAXACCEL = 120 95 | SCALE = 100 96 | FERROR = 20.0 97 | MIN_LIMIT = -202 98 | MAX_LIMIT = 2 99 | HOME_OFFSET = -10 100 | HOME_SEARCH_VEL = -5.0 101 | HOME_LATCH_VEL = 0.5 102 | HOME_IGNORE_LIMITS = NO 103 | HOME_USE_INDEX = NO 104 | HOME_SEQUENCE = 1 105 | 106 | [AXIS_Z] 107 | MIN_LIMIT = -202 108 | MAX_LIMIT = 2 109 | MAX_VELOCITY = 20.0 110 | MAX_ACCELERATION = 100 111 | 112 | [JOINT_2] 113 | TYPE = LINEAR 114 | HOME = -50 115 | MAX_VELOCITY = 20.0 116 | MAX_ACCELERATION = 100 117 | STEPGEN_MAXACCEL = 120 118 | SCALE = 100 119 | FERROR = 20.0 120 | MIN_LIMIT = -202 121 | MAX_LIMIT = 2 122 | HOME_OFFSET = -10 123 | HOME_SEARCH_VEL = -5.0 124 | HOME_LATCH_VEL = 0.5 125 | HOME_IGNORE_LIMITS = NO 126 | HOME_USE_INDEX = NO 127 | HOME_SEQUENCE = 0 128 | 129 | [STEPCONF] 130 | STEPLEN = 1000 131 | STEPSPACE = 10000 132 | DIRHOLD = 500 133 | DIRSETUP = 500 134 | -------------------------------------------------------------------------------- /examples/linuxcnc/dummy_phy/profibus.hal: -------------------------------------------------------------------------------- 1 | # --------------------------------------- 2 | # --- PROFIBUS DP 3 | # --- LinuxCNC HAL configuration file 4 | # ------ 5 | 6 | 7 | # --- Load the pyprofibus HAL userspace module --- 8 | # 9 | # Parameters: 10 | # --loglevel LVL : Change the log level. 11 | # 12 | # --nice NICE : Renice the process. -20 <= NICE <= 19. 13 | # Default: Do not renice. 14 | # 15 | # The last parameter is the configuration file describing the PROFIBUS. 16 | # 17 | loadusr -Wn profibus pyprofibus-linuxcnc-hal pyprofibus.conf 18 | 19 | 20 | # --- Enable the used master-to-slave pins --- 21 | # MOSI = MasterOutputSlaveInput = Data flow from PB master to PB slave. 22 | # All master-to-slave pins are deactivated by default. 23 | # So all master-to-slave pins connected below, should be activated here by writing 24 | # a '1' to the '.active' pin. 25 | # Deactivated master-to-slave pins will not be forwarded from LinuxCNC to the PROFIBUS. 26 | setp profibus.slave.8.mosi.bit.0.0.active 1 27 | setp profibus.slave.8.mosi.bit.0.1.active 1 28 | setp profibus.slave.8.mosi.bit.1.0.active 1 29 | setp profibus.slave.8.mosi.bit.1.1.active 1 30 | setp profibus.slave.8.mosi.bit.1.2.active 1 31 | setp profibus.slave.8.mosi.bit.1.3.active 1 32 | setp profibus.slave.8.mosi.bit.1.4.active 1 33 | setp profibus.slave.8.mosi.float.4.active 1 34 | setp profibus.slave.8.mosi.float.8.active 1 35 | setp profibus.slave.8.mosi.float.12.active 1 36 | setp profibus.slave.8.mosi.u8.16.active 1 37 | setp profibus.slave.8.mosi.u16.18.active 1 38 | setp profibus.slave.8.mosi.s16.20.active 1 39 | setp profibus.slave.8.mosi.u31.22.active 1 40 | setp profibus.slave.8.mosi.s32.26.active 1 41 | 42 | # --- Connect master-to-slave pins --- 43 | # MOSI = MasterOutputSlaveInput = Data flow from PB master to PB slave. 44 | net heartbeat => profibus.slave.8.mosi.bit.0.0 45 | net estop-out-not => profibus.slave.8.mosi.bit.0.1 46 | net spindle-cw => profibus.slave.8.mosi.bit.1.0 47 | net spindle-ccw => profibus.slave.8.mosi.bit.1.1 48 | net xpos-cmd => profibus.slave.8.mosi.float.4 49 | net ypos-cmd => profibus.slave.8.mosi.float.8 50 | net zpos-cmd => profibus.slave.8.mosi.float.12 51 | 52 | setp profibus.slave.8.mosi.bit.1.2 1 # limit-x inverted dummy loopback 53 | setp profibus.slave.8.mosi.bit.1.3 1 # limit-y inverted dummy loopback 54 | setp profibus.slave.8.mosi.bit.1.4 1 # limit-z inverted dummy loopback 55 | setp profibus.slave.8.mosi.bit.1.5 0 # estop-in-not inverted dummy loopback 56 | 57 | 58 | # --- Enable the used slave-to-master pins --- 59 | # MISO = MasterInputSlaveOutput = Data flow from PB slave to PB master. 60 | # All slave-to-master pins are deactivated by default. 61 | # So all slave-to-master pins connected below, should be activated here by writing 62 | # a '1' to the '.active' pin. 63 | # Deactivated slave-to-master pins will not be forwarded from the PROFIBUS to LinuxCNC. 64 | setp profibus.slave.8.miso.bit.0.0.active 1 65 | setp profibus.slave.8.miso.bit.0.1.active 1 66 | setp profibus.slave.8.miso.bit.1.0.active 1 67 | setp profibus.slave.8.miso.bit.1.1.active 1 68 | setp profibus.slave.8.miso.bit.1.2.active 1 69 | setp profibus.slave.8.miso.bit.1.3.active 1 70 | setp profibus.slave.8.miso.bit.1.4.active 1 71 | setp profibus.slave.8.miso.bit.1.5.active 1 72 | setp profibus.slave.8.miso.float.4.active 1 73 | setp profibus.slave.8.miso.float.8.active 1 74 | setp profibus.slave.8.miso.float.12.active 1 75 | setp profibus.slave.8.miso.u8.16.active 1 76 | setp profibus.slave.8.miso.u16.18.active 1 77 | setp profibus.slave.8.miso.s16.20.active 1 78 | setp profibus.slave.8.miso.u31.22.active 1 79 | setp profibus.slave.8.miso.s32.26.active 1 80 | 81 | # If the slave disconnects, then reset all slave-to-master pins to zero. 82 | setp profibus.slave.8.config.disconnect-clear-pins 1 83 | 84 | # --- Connect slave-to-master pins --- 85 | # MISO = MasterInputSlaveOutput = Data flow from PB slave to PB master. 86 | net limit-x <= profibus.slave.8.miso.bit.1.2 87 | net limit-y <= profibus.slave.8.miso.bit.1.3 88 | net limit-z <= profibus.slave.8.miso.bit.1.4 89 | net estop-in-not <= profibus.slave.8.miso.bit.1.5 90 | 91 | 92 | 93 | # Always keep this at the end of this file. 94 | # This will activate data transfer between pyprofibus and LinuxCNC. 95 | setp profibus.config.ready 1 96 | -------------------------------------------------------------------------------- /examples/linuxcnc/dummy_phy/pyprofibus.conf: -------------------------------------------------------------------------------- 1 | ; ----------------------------------------------- ; 2 | ; ; 3 | ; PROFIBUS configuration ; 4 | ; ; 5 | ; This file configures a pyprofibus instance. ; 6 | ; ; 7 | ; ----------------------------------------------- ; 8 | 9 | 10 | ; General settings 11 | [PROFIBUS] 12 | ; Enable/disable debug mode. 13 | ; 0 -> no debugging. 14 | ; 1 -> DP debugging. 15 | ; 2 -> DP and PHY debugging. 16 | debug=1 17 | 18 | 19 | ; PHY protocol layer configuration 20 | [PHY] 21 | 22 | ; The PHY layer driver type. 23 | type=dummy_slave 24 | 25 | ; The PHY device name/path. 26 | ; Can be a device like /dev/ttyS0 or /dev/ttyAMA0 for 'serial'. 27 | dev=/dev/ttyS0 28 | 29 | ; The Profibus on-wire baud rate. 30 | baud=19200 31 | 32 | 33 | ; FDL protocol layer configuration 34 | [FDL] 35 | 36 | 37 | ; DP protocol layer configuration 38 | [DP] 39 | 40 | ; The master device class. Either 1 or 2. 41 | master_class=1 42 | 43 | ; The Profibus address of this device. 44 | master_addr=2 45 | 46 | 47 | ; --- 48 | ; Slave configurations 49 | ; Add as many [SLAVE_xxx] sections as needed. 50 | ; --- 51 | 52 | ; First slave configuration 53 | [SLAVE_0] 54 | 55 | ; Optional slave name. Will be stored in slaveConf.name and slaveDesc.name. 56 | ; pyprofibus does not use the name internally. 57 | name=dummy_slave 58 | 59 | ; This slave's Profibus address 60 | addr=8 61 | 62 | ; The path to the GSD file. 63 | gsd=../../../misc/dummy_modular.gsd 64 | 65 | ; Boolean: Sync mode enabled/available? 66 | sync_mode=1 67 | 68 | ; Boolean: Freeze mode enabled/available? 69 | freeze_mode=1 70 | 71 | ; 8 bit integer specifying the Profibus group ident mask. 72 | group_mask=1 73 | 74 | ; This slave's watchdog timeout, in milliseconds. 75 | watchdog_ms=300 76 | 77 | ; Module configuration. 78 | ; For each module plugged into the slave, add a module_xxx 79 | ; entry with the name of the module. 80 | ; The module name must match the name from the GSD file (approximately). 81 | ; The modules are used in the order of the index number. 82 | module_0=dummy input module 83 | module_1=dummy output module 84 | 85 | ; The number of output bytes this slave transmits to the 86 | ; master in Data_Exchange. 87 | ; This usually depends on the modules plugged into the slave. 88 | output_size=32 89 | 90 | ; The number of input bytes this slave expects to receive 91 | ; in Data_Exchange. 92 | ; This usually depends on the modules plugged into the slave. 93 | input_size=40 94 | 95 | ; Request and interpret a slave diagnosis every n Data_Exchange telegrams. 96 | ; n defaults to 0, which means: Never periodically request diagnosis. 97 | ; If periodic diagnosis is switched off, then diagnostic information will only be requested on faults. 98 | ; Note that input-only slaves (output_size=0) probably need a non-zero diag_period. 99 | diag_period=0 100 | -------------------------------------------------------------------------------- /examples/linuxcnc/dummy_phy/run-linuxcnc-demo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | die() 4 | { 5 | echo "$*" >&2 6 | exit 1 7 | } 8 | 9 | usage() 10 | { 11 | echo "Usage: run-linuxcnc-demo.sh [/path/to/linuxcnc]" 12 | echo 13 | echo " /path/to/linuxcnc: Path to 'linuxcnc' start script" 14 | } 15 | 16 | if [ $# -ge 1 ] && [ "$1" = "-h" -o "$1" = "--help" ]; then 17 | usage 18 | exit 0 19 | fi 20 | if [ $# -eq 0 ]; then 21 | linuxcnc="linuxcnc" 22 | elif [ $# -eq 1 ]; then 23 | linuxcnc="$1" 24 | else 25 | usage 26 | exit 1 27 | fi 28 | 29 | 30 | # basedir = directory where this script lives in 31 | basedir="$(dirname "$0")" 32 | [ "$(echo "$basedir" | cut -c1)" = '/' ] || basedir="$PWD/$basedir" 33 | 34 | # rootdir = root of the pyprofibus repository 35 | rootdir="$basedir/../../.." 36 | 37 | [ -x "$rootdir/pyprofibus-linuxcnc-hal" ] || die "pyprofibus-linuxcnc-hal not found" 38 | 39 | cleanup() 40 | { 41 | rm -f "/tmp/linuxcnc-demo.ngc" 42 | } 43 | 44 | cleanup 45 | trap cleanup EXIT 46 | cp "$basedir/linuxcnc-demo.ngc" /tmp/ || die "Failed to copy linuxcnc-demo.ngc" 47 | 48 | # Start LinuxCNC 49 | ( 50 | cd "$basedir" || die "Failed to 'cd $basedir'" 51 | PATH="$rootdir/:$PATH"\ 52 | PYTHONPATH="$rootdir/:$PYTHONPATH"\ 53 | "$linuxcnc" "$basedir/linuxcnc-demo.ini" ||\ 54 | die "LinuxCNC exited with an error" 55 | ) 56 | -------------------------------------------------------------------------------- /examples/linuxcnc/et200s/.gitignore: -------------------------------------------------------------------------------- 1 | rs274ngc.var* 2 | emc.var* 3 | -------------------------------------------------------------------------------- /examples/linuxcnc/et200s/linuxcnc-demo.hal: -------------------------------------------------------------------------------- 1 | loadrt [KINS]KINEMATICS 2 | loadrt [EMCMOT]EMCMOT base_period_nsec=[EMCMOT]BASE_PERIOD servo_period_nsec=[EMCMOT]SERVO_PERIOD traj_period_nsec=[EMCMOT]SERVO_PERIOD key=[EMCMOT]SHMEM_KEY num_joints=[KINS]JOINTS 3 | 4 | loadrt stepgen step_type=0,0,0 5 | loadrt charge_pump 6 | loadrt comp count=3 7 | loadusr -W hal_manualtoolchange 8 | 9 | # Base thread 10 | addf stepgen.make-pulses base-thread 11 | 12 | # Servo thread 13 | addf stepgen.capture-position servo-thread 14 | addf motion-command-handler servo-thread 15 | addf motion-controller servo-thread 16 | addf stepgen.update-freq servo-thread 17 | addf charge-pump servo-thread 18 | addf comp.0 servo-thread 19 | addf comp.1 servo-thread 20 | addf comp.2 servo-thread 21 | 22 | # Spindle control 23 | net spindle-cw <= spindle.0.forward 24 | net spindle-ccw <= spindle.0.reverse 25 | 26 | # Coolant control 27 | net coolant-mist <= iocontrol.0.coolant-mist 28 | net coolant-flood <= iocontrol.0.coolant-flood 29 | 30 | # heartbeat generator 31 | setp charge-pump.enable 1 32 | net heartbeat <= charge-pump.out-4 33 | 34 | # Emergency stop logic 35 | net estop-out-not <= iocontrol.0.user-enable-out 36 | net estop-in-not => iocontrol.0.emc-enable-in 37 | 38 | # Manual tool change 39 | net tool-change iocontrol.0.tool-change => hal_manualtoolchange.change 40 | net tool-changed iocontrol.0.tool-changed <= hal_manualtoolchange.changed 41 | net tool-number iocontrol.0.tool-prep-number => hal_manualtoolchange.number 42 | net tool-prepare-loopback iocontrol.0.tool-prepare => iocontrol.0.tool-prepared 43 | 44 | # X axis 45 | setp stepgen.0.position-scale [JOINT_0]SCALE 46 | setp stepgen.0.steplen [STEPCONF]STEPLEN 47 | setp stepgen.0.stepspace [STEPCONF]STEPSPACE 48 | setp stepgen.0.dirhold [STEPCONF]DIRHOLD 49 | setp stepgen.0.dirsetup [STEPCONF]DIRSETUP 50 | setp stepgen.0.maxaccel [JOINT_0]STEPGEN_MAXACCEL 51 | net xpos-cmd joint.0.motor-pos-cmd => stepgen.0.position-cmd 52 | net xpos-fb stepgen.0.position-fb => joint.0.motor-pos-fb 53 | net xstep <= stepgen.0.step 54 | net xdir <= stepgen.0.dir 55 | net xenable joint.0.amp-enable-out => stepgen.0.enable 56 | net home-x => joint.0.home-sw-in 57 | net limit-x => joint.0.neg-lim-sw-in 58 | net limit-x => joint.0.pos-lim-sw-in 59 | 60 | # Y axis 61 | setp stepgen.1.position-scale [JOINT_1]SCALE 62 | setp stepgen.1.steplen [STEPCONF]STEPLEN 63 | setp stepgen.1.stepspace [STEPCONF]STEPSPACE 64 | setp stepgen.1.dirhold [STEPCONF]DIRHOLD 65 | setp stepgen.1.dirsetup [STEPCONF]DIRSETUP 66 | setp stepgen.1.maxaccel [JOINT_1]STEPGEN_MAXACCEL 67 | net ypos-cmd joint.1.motor-pos-cmd => stepgen.1.position-cmd 68 | net ypos-fb stepgen.1.position-fb => joint.1.motor-pos-fb 69 | net ystep <= stepgen.1.step 70 | net ydir <= stepgen.1.dir 71 | net yenable joint.1.amp-enable-out => stepgen.1.enable 72 | net home-y => joint.1.home-sw-in 73 | net limit-y => joint.1.neg-lim-sw-in 74 | net limit-y => joint.1.pos-lim-sw-in 75 | 76 | # Z axis 77 | setp stepgen.2.position-scale [JOINT_2]SCALE 78 | setp stepgen.2.steplen [STEPCONF]STEPLEN 79 | setp stepgen.2.stepspace [STEPCONF]STEPSPACE 80 | setp stepgen.2.dirhold [STEPCONF]DIRHOLD 81 | setp stepgen.2.dirsetup [STEPCONF]DIRSETUP 82 | setp stepgen.2.maxaccel [JOINT_2]STEPGEN_MAXACCEL 83 | net zpos-cmd joint.2.motor-pos-cmd => stepgen.2.position-cmd 84 | net zpos-fb stepgen.2.position-fb => joint.2.motor-pos-fb 85 | net zstep <= stepgen.2.step 86 | net zdir <= stepgen.2.dir 87 | net zenable joint.2.amp-enable-out => stepgen.2.enable 88 | net home-z => joint.2.home-sw-in 89 | net limit-z => joint.2.neg-lim-sw-in 90 | net limit-z => joint.2.pos-lim-sw-in 91 | 92 | # Simulate home switches 93 | net xpos-cmd => comp.0.in0 94 | setp comp.0.in1 0 95 | setp comp.0.hyst 0.1 96 | net home-x <= comp.0.out 97 | 98 | net ypos-cmd => comp.1.in0 99 | setp comp.1.in1 0 100 | setp comp.1.hyst 0.1 101 | net home-y <= comp.1.out 102 | 103 | net zpos-cmd => comp.2.in0 104 | setp comp.2.in1 0 105 | setp comp.2.hyst 0.1 106 | net home-z <= comp.2.out 107 | 108 | # Simulate hardware limit switches 109 | sets limit-x 0 110 | sets limit-y 0 111 | sets limit-z 0 112 | -------------------------------------------------------------------------------- /examples/linuxcnc/et200s/linuxcnc-demo.ini: -------------------------------------------------------------------------------- 1 | [EMC] 2 | VERSION = 1.1 3 | MACHINE = linuxcnc-demo 4 | DEBUG = 0 5 | 6 | [DISPLAY] 7 | DISPLAY = axis 8 | POSITION_OFFSET = RELATIVE 9 | POSITION_FEEDBACK = ACTUAL 10 | MAX_FEED_OVERRIDE = 1.2 11 | INTRO_GRAPHIC = emc2.gif 12 | INTRO_TIME = 0 13 | PROGRAM_PREFIX = /tmp 14 | INCREMENTS = 0.1mm 0.05mm 0.01mm 0.005mm 0.001mm 15 | OPEN_FILE = linuxcnc-demo.ngc 16 | 17 | [FILTER] 18 | PROGRAM_EXTENSION = .py Python script 19 | py = python3 20 | 21 | [RS274NGC] 22 | PARAMETER_FILE = emc.var 23 | RS274NGC_STARTUP_CODE = G17 G21 G40 G49 G80 G90 G94 24 | 25 | [EMCMOT] 26 | EMCMOT = motmod 27 | SHMEM_KEY = 111 28 | COMM_TIMEOUT = 1.0 29 | BASE_PERIOD = 20000 30 | SERVO_PERIOD = 1000000 31 | 32 | [TASK] 33 | TASK = milltask 34 | CYCLE_TIME = 0.001 35 | 36 | [HAL] 37 | HALUI = halui 38 | HALFILE = linuxcnc-demo.hal 39 | HALFILE = profibus.hal 40 | 41 | [HALUI] 42 | 43 | [TRAJ] 44 | COORDINATES = XYZ 45 | LINEAR_UNITS = mm 46 | ANGULAR_UNITS = degree 47 | DEFAULT_LINEAR_VELOCITY = 10.0 48 | MAX_LINEAR_VELOCITY = 30.0 49 | NO_FORCE_HOMING = 1 50 | 51 | [EMCIO] 52 | EMCIO = io 53 | CYCLE_TIME = 0.100 54 | TOOL_TABLE = tool.tbl 55 | 56 | [KINS] 57 | KINEMATICS = trivkins coordinates=XYZ 58 | JOINTS = 3 59 | 60 | [AXIS_X] 61 | MIN_LIMIT = -202 62 | MAX_LIMIT = 2 63 | MAX_VELOCITY = 20.0 64 | MAX_ACCELERATION = 100 65 | 66 | [JOINT_0] 67 | TYPE = LINEAR 68 | HOME = -100 69 | MAX_VELOCITY = 20.0 70 | MAX_ACCELERATION = 100 71 | STEPGEN_MAXACCEL = 120 72 | SCALE = 100 73 | FERROR = 20.0 74 | MIN_LIMIT = -202 75 | MAX_LIMIT = 2 76 | HOME_OFFSET = -10 77 | HOME_SEARCH_VEL = -5.0 78 | HOME_LATCH_VEL = -0.5 79 | HOME_IGNORE_LIMITS = NO 80 | HOME_USE_INDEX = NO 81 | HOME_SEQUENCE = 1 82 | 83 | [AXIS_Y] 84 | MIN_LIMIT = -202 85 | MAX_LIMIT = 2 86 | MAX_VELOCITY = 20.0 87 | MAX_ACCELERATION = 100 88 | 89 | [JOINT_1] 90 | TYPE = LINEAR 91 | HOME = -100 92 | MAX_VELOCITY = 20.0 93 | MAX_ACCELERATION = 100 94 | STEPGEN_MAXACCEL = 120 95 | SCALE = 100 96 | FERROR = 20.0 97 | MIN_LIMIT = -202 98 | MAX_LIMIT = 2 99 | HOME_OFFSET = -10 100 | HOME_SEARCH_VEL = -5.0 101 | HOME_LATCH_VEL = 0.5 102 | HOME_IGNORE_LIMITS = NO 103 | HOME_USE_INDEX = NO 104 | HOME_SEQUENCE = 1 105 | 106 | [AXIS_Z] 107 | MIN_LIMIT = -202 108 | MAX_LIMIT = 2 109 | MAX_VELOCITY = 20.0 110 | MAX_ACCELERATION = 100 111 | 112 | [JOINT_2] 113 | TYPE = LINEAR 114 | HOME = -50 115 | MAX_VELOCITY = 20.0 116 | MAX_ACCELERATION = 100 117 | STEPGEN_MAXACCEL = 120 118 | SCALE = 100 119 | FERROR = 20.0 120 | MIN_LIMIT = -202 121 | MAX_LIMIT = 2 122 | HOME_OFFSET = -10 123 | HOME_SEARCH_VEL = -5.0 124 | HOME_LATCH_VEL = 0.5 125 | HOME_IGNORE_LIMITS = NO 126 | HOME_USE_INDEX = NO 127 | HOME_SEQUENCE = 0 128 | 129 | [STEPCONF] 130 | STEPLEN = 1000 131 | STEPSPACE = 10000 132 | DIRHOLD = 500 133 | DIRSETUP = 500 134 | -------------------------------------------------------------------------------- /examples/linuxcnc/et200s/profibus.hal: -------------------------------------------------------------------------------- 1 | # --------------------------------------- 2 | # --- PROFIBUS DP 3 | # --- LinuxCNC HAL configuration file 4 | # ------ 5 | 6 | 7 | # --- Load the pyprofibus HAL userspace module --- 8 | # 9 | # Parameters: 10 | # --loglevel LVL : Change the log level. 11 | # 12 | # --nice NICE : Renice the process. -20 <= NICE <= 19. 13 | # Default: Do not renice. 14 | # 15 | # The last parameter is the configuration file describing the PROFIBUS. 16 | # 17 | loadusr -Wn profibus pyprofibus-linuxcnc-hal pyprofibus.conf 18 | 19 | 20 | # --- Enable the used master-to-slave pins --- 21 | # MOSI = MasterOutputSlaveInput = Data flow from PB master to PB slave. 22 | # All master-to-slave pins are deactivated by default. 23 | # So all master-to-slave pins connected below, should be activated here by writing 24 | # a '1' to the '.active' pin. 25 | # Deactivated master-to-slave pins will not be forwarded from LinuxCNC to the PROFIBUS. 26 | setp profibus.slave.8.mosi.bit.0.0.active 1 27 | setp profibus.slave.8.mosi.bit.0.1.active 1 28 | setp profibus.slave.8.mosi.bit.1.0.active 1 29 | setp profibus.slave.8.mosi.bit.1.1.active 1 30 | #setp profibus.slave.8.mosi.float.4.active 1 31 | #setp profibus.slave.8.mosi.float.8.active 1 32 | #setp profibus.slave.8.mosi.float.12.active 1 33 | 34 | # --- Connect master-to-slave pins --- 35 | # MOSI = MasterOutputSlaveInput = Data flow from PB master to PB slave. 36 | net heartbeat => profibus.slave.8.mosi.bit.0.0 37 | net estop-out-not => profibus.slave.8.mosi.bit.0.1 38 | net spindle-cw => profibus.slave.8.mosi.bit.1.0 39 | net spindle-ccw => profibus.slave.8.mosi.bit.1.1 40 | #net xpos-cmd => profibus.slave.8.mosi.float.4 41 | #net ypos-cmd => profibus.slave.8.mosi.float.8 42 | #net zpos-cmd => profibus.slave.8.mosi.float.12 43 | 44 | 45 | # --- Enable the used slave-to-master pins --- 46 | # MISO = MasterInputSlaveOutput = Data flow from PB slave to PB master. 47 | # All slave-to-master pins are deactivated by default. 48 | # So all slave-to-master pins connected below, should be activated here by writing 49 | # a '1' to the '.active' pin. 50 | # Deactivated slave-to-master pins will not be forwarded from the PROFIBUS to LinuxCNC. 51 | setp profibus.slave.8.miso.bit.0.0.active 1 52 | setp profibus.slave.8.miso.bit.0.1.active 1 53 | setp profibus.slave.8.miso.bit.0.2.active 1 54 | setp profibus.slave.8.miso.bit.0.3.active 1 55 | 56 | # If the slave disconnects, then reset all slave-to-master pins to zero. 57 | setp profibus.slave.8.config.disconnect-clear-pins 1 58 | 59 | # --- Connect slave-to-master pins --- 60 | # MISO = MasterInputSlaveOutput = Data flow from PB slave to PB master. 61 | net limit-x <= profibus.slave.8.miso.bit.0.0 62 | net limit-y <= profibus.slave.8.miso.bit.0.1 63 | net limit-z <= profibus.slave.8.miso.bit.0.2 64 | net estop-in-not <= profibus.slave.8.miso.bit.0.3 65 | 66 | 67 | 68 | # Always keep this at the end of this file. 69 | # This will activate data transfer between pyprofibus and LinuxCNC. 70 | setp profibus.config.ready 1 71 | -------------------------------------------------------------------------------- /examples/linuxcnc/et200s/pyprofibus.conf: -------------------------------------------------------------------------------- 1 | ; ----------------------------------------------- ; 2 | ; ; 3 | ; PROFIBUS configuration ; 4 | ; ; 5 | ; This file configures a pyprofibus instance. ; 6 | ; ; 7 | ; ----------------------------------------------- ; 8 | 9 | 10 | ; General settings 11 | [PROFIBUS] 12 | ; Enable/disable debug mode. 13 | ; 0 -> no debugging. 14 | ; 1 -> DP debugging. 15 | ; 2 -> DP and PHY debugging. 16 | debug=1 17 | 18 | 19 | ; PHY protocol layer configuration 20 | [PHY] 21 | 22 | ; The PHY layer driver type. 23 | type=serial 24 | 25 | ; The PHY device name/path. 26 | ; Can be a device like /dev/ttyS0 or /dev/ttyAMA0 for 'serial'. 27 | dev=/dev/ttyS0 28 | 29 | ; The Profibus on-wire baud rate. 30 | baud=19200 31 | 32 | 33 | ; FDL protocol layer configuration 34 | [FDL] 35 | 36 | 37 | ; DP protocol layer configuration 38 | [DP] 39 | 40 | ; The master device class. Either 1 or 2. 41 | master_class=1 42 | 43 | ; The Profibus address of this device. 44 | master_addr=2 45 | 46 | 47 | ; --- 48 | ; Slave configurations 49 | ; Add as many [SLAVE_xxx] sections as needed. 50 | ; --- 51 | 52 | ; First slave configuration 53 | [SLAVE_0] 54 | 55 | ; Optional slave name. Will be stored in slaveConf.name and slaveDesc.name. 56 | ; pyprofibus does not use the name internally. 57 | name=et200s 58 | 59 | ; This slave's Profibus address 60 | addr=8 61 | 62 | ; The path to the GSD file. 63 | gsd=si03806a.gsd 64 | 65 | ; Boolean: Sync mode enabled/available? 66 | sync_mode=1 67 | 68 | ; Boolean: Freeze mode enabled/available? 69 | freeze_mode=1 70 | 71 | ; 8 bit integer specifying the Profibus group ident mask. 72 | group_mask=1 73 | 74 | ; This slave's watchdog timeout, in milliseconds. 75 | watchdog_ms=300 76 | 77 | ; Module configuration. 78 | ; For each module plugged into the slave, add a module_xxx 79 | ; entry with the name of the module. 80 | ; The module name must match the name from the GSD file (approximately). 81 | ; The modules are used in the order of the index number. 82 | module_0=6ES7 138-4CA01-0AA0 PM-E DC24V 83 | module_1=6ES7 132-4BB30-0AA0 2DO DC24V 84 | module_2=6ES7 132-4BB30-0AA0 2DO DC24V 85 | module_3=6ES7 131-4BD01-0AA0 4DI DC24V 86 | 87 | ; The number of output bytes this slave transmits to the 88 | ; master in Data_Exchange. 89 | ; This usually depends on the modules plugged into the slave. 90 | output_size=1 91 | 92 | ; The number of input bytes this slave expects to receive 93 | ; in Data_Exchange. 94 | ; This usually depends on the modules plugged into the slave. 95 | input_size=2 96 | 97 | ; Request and interpret a slave diagnosis every n Data_Exchange telegrams. 98 | ; n defaults to 0, which means: Never periodically request diagnosis. 99 | ; If periodic diagnosis is switched off, then diagnostic information will only be requested on faults. 100 | ; Note that input-only slaves (output_size=0) probably need a non-zero diag_period. 101 | diag_period=0 102 | -------------------------------------------------------------------------------- /examples/linuxcnc/et200s/run-linuxcnc-demo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | die() 4 | { 5 | echo "$*" >&2 6 | exit 1 7 | } 8 | 9 | usage() 10 | { 11 | echo "Usage: run-linuxcnc-demo.sh [/path/to/linuxcnc]" 12 | echo 13 | echo " /path/to/linuxcnc: Path to 'linuxcnc' start script" 14 | } 15 | 16 | if [ $# -ge 1 ] && [ "$1" = "-h" -o "$1" = "--help" ]; then 17 | usage 18 | exit 0 19 | fi 20 | if [ $# -eq 0 ]; then 21 | linuxcnc="linuxcnc" 22 | elif [ $# -eq 1 ]; then 23 | linuxcnc="$1" 24 | else 25 | usage 26 | exit 1 27 | fi 28 | 29 | 30 | # basedir = directory where this script lives in 31 | basedir="$(dirname "$0")" 32 | [ "$(echo "$basedir" | cut -c1)" = '/' ] || basedir="$PWD/$basedir" 33 | 34 | # rootdir = root of the pyprofibus repository 35 | rootdir="$basedir/../../.." 36 | 37 | [ -x "$rootdir/pyprofibus-linuxcnc-hal" ] || die "pyprofibus-linuxcnc-hal not found" 38 | 39 | cleanup() 40 | { 41 | rm -f "/tmp/linuxcnc-demo.ngc" 42 | } 43 | 44 | cleanup 45 | trap cleanup EXIT 46 | cp "$basedir/linuxcnc-demo.ngc" /tmp/ || die "Failed to copy linuxcnc-demo.ngc" 47 | 48 | # Start LinuxCNC 49 | ( 50 | cd "$basedir" || die "Failed to 'cd $basedir'" 51 | PATH="$rootdir/:$PATH"\ 52 | PYTHONPATH="$rootdir/:$PYTHONPATH"\ 53 | "$linuxcnc" "$basedir/linuxcnc-demo.ini" ||\ 54 | die "LinuxCNC exited with an error" 55 | ) 56 | -------------------------------------------------------------------------------- /gsdparser: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | # 4 | # PROFIBUS - GSD file parser 5 | # 6 | # Copyright (c) 2016-2020 Michael Buesch 7 | # 8 | # Licensed under the terms of the GNU General Public License version 2, 9 | # or (at your option) any later version. 10 | # 11 | """ 12 | 13 | from __future__ import division, absolute_import, print_function, unicode_literals 14 | 15 | import sys 16 | from pyprofibus.gsd.interp import GsdInterp, GsdError 17 | 18 | import sys 19 | import getopt 20 | 21 | 22 | def usage(): 23 | print("GSD file parser") 24 | print("") 25 | print("Usage: gsdparser [OPTIONS] [ACTIONS] FILE.GSD") 26 | print("") 27 | print("FILE.GSD is the GSD file to parse.") 28 | print("") 29 | print("Options:") 30 | print(" -o|--output FILE Write output to FILE instead of stdout.") 31 | print(" -d|--debug Enable parser debugging (default off).") 32 | print(" -h|--help Show this help.") 33 | print("") 34 | print("Actions:") 35 | print(" -S|--summary Print a summary of the GSD file contents.") 36 | print(" (The default, if no action is specified)") 37 | print(" -D|--dump Dump the GSD data structure as Python code.") 38 | print("") 39 | print("Options for --dump:") 40 | print(" --dump-strip Strip leading and trailing whitespace from strings.") 41 | print(" --dump-notext Do not dump PrmText.") 42 | print(" --dump-noextuserprmdata Discard all ExtUserPrmData and ExtUserPrmDataRef.") 43 | print(" --dump-module NAME Only dump this module. (default: Dump all)") 44 | print(" Can be specified more then once to dump multiple modules.") 45 | 46 | def out(fd, text): 47 | fd.write(text) 48 | fd.flush() 49 | 50 | def main(): 51 | opt_output = None 52 | opt_debug = False 53 | opt_dumpStrip = False 54 | opt_dumpNoText = False 55 | opt_dumpNoExtUserPrmData = False 56 | opt_dumpModules = [] 57 | actions = [] 58 | 59 | try: 60 | (opts, args) = getopt.getopt(sys.argv[1:], 61 | "ho:dSD", 62 | [ "help", 63 | "output=", 64 | "debug", 65 | "summary", 66 | "dump", 67 | "dump-strip", 68 | "dump-notext", 69 | "dump-noextuserprmdata", 70 | "dump-module=", ]) 71 | except getopt.GetoptError as e: 72 | sys.stderr.write(str(e) + "\n") 73 | usage() 74 | return 1 75 | for (o, v) in opts: 76 | if o in ("-h", "--help"): 77 | usage() 78 | return 0 79 | if o in ("-o", "--output"): 80 | opt_output = v 81 | if o in ("-d", "--debug"): 82 | opt_debug = True 83 | if o in ("-S", "--summary"): 84 | actions.append( ("summary", None) ) 85 | if o in ("-D", "--dump"): 86 | actions.append( ("dump", None) ) 87 | if o in ("--dump-strip", ): 88 | opt_dumpStrip = True 89 | if o in ("--dump-notext", ): 90 | opt_dumpNoText = True 91 | if o in ("--dump-noextuserprmdata", ): 92 | opt_dumpNoExtUserPrmData = True 93 | if o in ("--dump-module", ): 94 | opt_dumpModules.append(v) 95 | if len(args) != 1: 96 | usage() 97 | return 1 98 | gsdFile = args[0] 99 | if not actions: 100 | actions = [ ("summary", None), ] 101 | 102 | try: 103 | if opt_output is None: 104 | outFd = sys.stdout 105 | else: 106 | outFd = open(opt_output, "w", encoding="UTF-8") 107 | except OSError as e: 108 | sys.stderr.write("ERROR: %s\n" % str(e)) 109 | return 1 110 | try: 111 | 112 | interp = GsdInterp.fromFile(gsdFile, debug=opt_debug) 113 | for action, v in actions: 114 | if action == "summary": 115 | out(outFd, str(interp)) 116 | elif action == "dump": 117 | py = interp.dumpPy(stripStr=opt_dumpStrip, 118 | noText=opt_dumpNoText, 119 | noExtUserPrmData=opt_dumpNoExtUserPrmData, 120 | modules=(opt_dumpModules or None)) 121 | out(outFd, py) 122 | else: 123 | assert(0) 124 | except GsdError as e: 125 | sys.stderr.write("ERROR: %s\n" % str(e)) 126 | return 1 127 | except Exception as e: 128 | sys.stderr.write("Exception: %s\n" % str(e)) 129 | return 1 130 | finally: 131 | if opt_output is not None: 132 | outFd.flush() 133 | outFd.close() 134 | return 0 135 | 136 | if __name__ == "__main__": 137 | sys.exit(main()) 138 | -------------------------------------------------------------------------------- /maintenance/gen-doc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Generate documentation 4 | # 5 | 6 | 7 | basedir="$(dirname "$0")" 8 | [ "$(echo "$basedir" | cut -c1)" = '/' ] || basedir="$PWD/$basedir" 9 | 10 | srcdir="$basedir/.." 11 | 12 | 13 | die() 14 | { 15 | echo "$*" >&2 16 | exit 1 17 | } 18 | 19 | gen() 20 | { 21 | local rst="$1" 22 | local docname="$(basename "$rst" .rst)" 23 | local dir="$(dirname "$rst")" 24 | local html="$dir/$docname.html" 25 | local tmpfile="$(mktemp --tmpdir=/tmp --suffix=.rst)" 26 | 27 | echo "Generating $(realpath --relative-to="$srcdir" "$html") from $(realpath --relative-to="$srcdir" "$rst") ..." 28 | 29 | sed -e 's|\.rst>`_|.html>`_|g' "$rst" > "$tmpfile" ||\ 30 | die "Failed to generate" 31 | python3 -m readme_renderer -o "$html" "$tmpfile" ||\ 32 | die "Failed to generate" 33 | rm "$tmpfile" ||\ 34 | die "Failed to delete temporary file" 35 | } 36 | 37 | for i in $(find "$srcdir" \( -name submodules -prune \) -o \( -name release-archives -prune \) -o \( -name build -prune \) -o \( -name toolchain-build -prune \) -o \( -name crcgen -prune \) -o \( -name '*.rst' -print \)); do 38 | gen "$i" 39 | done 40 | 41 | exit 0 42 | -------------------------------------------------------------------------------- /maintenance/makerelease.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | srcdir="$(dirname "$0")" 4 | [ "$(echo "$srcdir" | cut -c1)" = '/' ] || srcdir="$PWD/$srcdir" 5 | 6 | srcdir="$srcdir/.." 7 | 8 | die() { echo "$*"; exit 1; } 9 | 10 | # Import the makerelease.lib 11 | # http://bues.ch/gitweb?p=misc.git;a=blob_plain;f=makerelease.lib;hb=HEAD 12 | for path in $(echo "$PATH" | tr ':' ' '); do 13 | [ -f "$MAKERELEASE_LIB" ] && break 14 | MAKERELEASE_LIB="$path/makerelease.lib" 15 | done 16 | [ -f "$MAKERELEASE_LIB" ] && . "$MAKERELEASE_LIB" || die "makerelease.lib not found." 17 | 18 | hook_get_version() 19 | { 20 | local file="$1/pyprofibus/version.py" 21 | local maj="$(cat "$file" | grep -Ee'^VERSION_MAJOR\s+=\s+' | head -n1 | awk '{print $3;}')" 22 | local min="$(cat "$file" | grep -Ee '^VERSION_MINOR\s+=\s+' | head -n1 | awk '{print $3;}')" 23 | local ext="$(cat "$file" | grep -Ee '^VERSION_EXTRA\s+=\s+' | head -n1 | awk '{print $3;}' | cut -d'"' -f2)" 24 | version="${maj}.${min}${ext}" 25 | } 26 | 27 | hook_post_checkout() 28 | { 29 | info "Pulling in git submodules" 30 | git submodule update --init --recursive 31 | 32 | default_hook_post_checkout "$@" 33 | 34 | rm -r "$1"/maintenance 35 | } 36 | 37 | hook_testbuild() 38 | { 39 | default_hook_testbuild "$@" 40 | 41 | if which mpy-cross >/dev/null 2>&1; then 42 | # Try a Micropython test build. 43 | sh "$1/micropython/install.sh" --clean --build-only 44 | else 45 | warn "mpy-cross not available. Skipping Micropython test." 46 | fi 47 | } 48 | 49 | hook_regression_tests() 50 | { 51 | default_hook_regression_tests "$@" 52 | 53 | # Run selftests 54 | sh "$1/tests/run.sh" 55 | } 56 | 57 | do_build() 58 | { 59 | local target="$1" 60 | local checkout_dir="$2" 61 | 62 | local builddir="$checkout_dir/phy_fpga" 63 | local bindir="$builddir/bin/$target" 64 | 65 | make -C "$builddir" TARGET="$target" 66 | 67 | mkdir -p "$bindir" 68 | for ftype in .bin .asc .blif .rpt .json _yosys.log _nextpnr.log; do 69 | local binfile="${target}_pyprofibusphy${ftype}" 70 | cp "$builddir/$binfile" "$bindir/$binfile" 71 | done 72 | mv "$builddir/${target}_program.sh" "$bindir/${target}_program.sh" 73 | 74 | make -C "$builddir" TARGET="$target" clean 75 | } 76 | 77 | hook_pre_archives() 78 | { 79 | local archive_dir="$1" 80 | local checkout_dir="$2" 81 | 82 | do_build tinyfpga_bx "$checkout_dir" 83 | } 84 | 85 | hook_doc_archives() 86 | { 87 | local archive_dir="$1" 88 | local checkout_dir="$2" 89 | 90 | local doc_name="$project-doc-$version" 91 | local doc_dir="$tmpdir/$doc_name" 92 | mkdir "$doc_dir" ||\ 93 | die "Failed to create directory '$doc_dir'" 94 | ( 95 | cd "$checkout_dir" || die "Failed to cd '$checkout_dir'" 96 | rsync --recursive --prune-empty-dirs \ 97 | --include='/doc/' \ 98 | --include='/doc/**/' \ 99 | --include='/doc/**.pdf' \ 100 | --include='/doc/**.png' \ 101 | --include='/doc/**.jpg' \ 102 | --include='/doc/**.jpeg' \ 103 | --include='/doc/**.1' \ 104 | --include='/doc/**.html' \ 105 | --include='/doc/**.txt' \ 106 | --include='/doc/**.ods' \ 107 | --include='/doc/**/README' \ 108 | --include='/micropython/' \ 109 | --include='/micropython/**/' \ 110 | --include='/micropython/**.html' \ 111 | --include='/*.html' \ 112 | --include='/*.txt' \ 113 | --exclude='*' \ 114 | . "$doc_dir" ||\ 115 | die "Failed to copy documentation." 116 | cd "$tmpdir" || die "Failed to cd '$tmpdir'" 117 | tar --owner=root --group=root -c -J -f "$archive_dir"/$doc_name.tar.xz \ 118 | "$doc_name" ||\ 119 | die "Failed to create doc archive." 120 | ) || die 121 | } 122 | 123 | project=pyprofibus 124 | default_archives=py-sdist-xz 125 | makerelease "$@" 126 | -------------------------------------------------------------------------------- /maintenance/update-submodules: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | basedir="$(dirname "$0")" 4 | [ "$(echo "$basedir" | cut -c1)" = '/' ] || basedir="$PWD/$basedir" 5 | 6 | 7 | die() 8 | { 9 | echo "$*" >&2 10 | exit 1 11 | } 12 | 13 | echo "Updating git submodules..." 14 | cd "$basedir/.." || die "Failed to cd to basedir" 15 | git submodule update --init --recursive || die "git submodule update failed" 16 | 17 | exit 0 18 | -------------------------------------------------------------------------------- /micropython/Makefile: -------------------------------------------------------------------------------- 1 | PYS := 2 | BOOTPY := boot.py 3 | MAINPY := main.py 4 | MAINPYDIR := 5 | GSDS := 6 | CONFS := 7 | 8 | SRCDIR := 9 | BUILDDIR := 10 | 11 | MARCH := xtensawin 12 | MPYCROSS_OPTS := -O0 13 | GSDPARSER_OPTS := --dump-strip --dump-notext --dump-noextuserprmdata 14 | 15 | MPYCROSS := mpy-cross 16 | GSDPARSER := $(SRCDIR)/gsdparser 17 | MKDIR := mkdir 18 | CP := cp 19 | RM := rm 20 | 21 | _MPYS = $(sort $(patsubst %.py,$(2)/%.mpy,$(1))) 22 | _GSDMPYS = $(sort $(patsubst %.gsd,$(2)/%_gsd.mpy,$(1))) 23 | _GSDPYS = $(sort $(patsubst %.gsd,$(2)/%_gsd.py,$(1))) 24 | _CONFS = $(sort $(patsubst %.conf,$(2)/%.conf,$(1))) 25 | _MPYCROSS = $(MPYCROSS) "$(1)" -s "$(2)" -march="$(MARCH)" $(MPYCROSS_OPTS) -o "$(3)" 26 | 27 | all: $(call _MPYS,$(PYS),$(BUILDDIR)) \ 28 | $(call _GSDMPYS,$(GSDS),$(BUILDDIR)) \ 29 | $(call _CONFS,$(CONFS),$(BUILDDIR)) \ 30 | $(BUILDDIR)/$(BOOTPY) \ 31 | $(BUILDDIR)/$(MAINPY) 32 | 33 | $(BUILDDIR)/%.mpy: %.py 34 | @$(MKDIR) -p "$(dir $@)" 35 | $(call _MPYCROSS,$<,$(shell basename "$<"),$@) 36 | 37 | $(BUILDDIR)/%_gsd.mpy: %.gsd 38 | @$(MKDIR) -p "$(dir $@)" 39 | $(GSDPARSER) -D $(GSDPARSER_OPTS) -o "$(call _GSDPYS,$<,$(BUILDDIR))" "$<" 40 | $(call _MPYCROSS,$(call _GSDPYS,$<,$(BUILDDIR)),$(shell basename "$(call _GSDPYS,$<,$(BUILDDIR))"),$@) 41 | $(RM) "$(call _GSDPYS,$<,$(BUILDDIR))" 42 | 43 | $(BUILDDIR)/%.conf: %.conf 44 | @$(MKDIR) -p "$(dir $@)" 45 | $(CP) "$<" "$@" 46 | 47 | $(BUILDDIR)/$(BOOTPY) $(BUILDDIR)/$(MAINPY): $(BUILDDIR)/%.py: $(MAINPYDIR)/%.py 48 | @$(MKDIR) -p "$(dir $@)" 49 | $(CP) "$<" "$@" 50 | 51 | clean: 52 | $(RM) -rf $(BUILDDIR) 53 | 54 | .PHONY: all clean 55 | -------------------------------------------------------------------------------- /micropython/README.rst: -------------------------------------------------------------------------------- 1 | pyprofibus Micropython support 2 | ============================== 3 | 4 | This directory contains scripts and support files for pyprofibus on `Micropython `_. 5 | 6 | 7 | Installing pyprofibus on a Micropython device 8 | ============================================= 9 | 10 | Run the install.sh script to build and install pyprofibus to a Micropython device via USB-UART. 11 | 12 | Run `install.sh -h` for more help. 13 | 14 | `install.sh` prepares and cross-compiles all pyprofibus files for the target platform to optimize resource usage. 15 | 16 | Example 17 | ------- 18 | 19 | Your device is an ESP32 (xtensawin) connected to the computer as /dev/ttyUSB0. 20 | Two GSD modules are configured in your pyprofibus .conf file. For example: 21 | 22 | * 6ES7 138-4CA01-0AA0 PM-E DC24V 23 | * 6ES7 132-4BB30-0AA0 2DO DC24V 24 | 25 | The corresponding command to build and install pyprofibus is: 26 | 27 | .. code:: sh 28 | 29 | ./micropython/install.sh --march xtensawin --module "6ES7 138-4CA01-0AA0 PM-E DC24V" --module "6ES7 132-4BB30-0AA0 2DO DC24V" /dev/ttyUSB0 30 | 31 | Prerequisites 32 | ============= 33 | 34 | The following tools have to be available on your Linux compatible system to build pyprofibus for Micropython: 35 | 36 | * The latest version of Micropython has to be installed on your device. See `these instructions `_ for installing Micropython on an ESP32 device. 37 | * `mpy-cross`: `Micropython cross compiler `_. 38 | * `pyboard.py`: Device flashing script `from Micropython distribution `_ to copy pyprofibus to the device. 39 | * `make`: `GNU make `_. 40 | 41 | These tools must be available in your PATH. 42 | 43 | You may pass the parameters `--pyboard` and/or `--mpycross` to `install.sh` to specify the path to your tool installation location. 44 | 45 | 46 | Resource usage 47 | ============== 48 | 49 | pyprofibus requires a fair amount of memory to run. 50 | 51 | Currently about 100 kBytes of memory available to the Micropython Garbage Collector are required to run pyprofibus. The more memory is available, the better. Remember that your application code also has to run in addition to pyprofibus. 52 | 53 | You can check the memory available to the GC with the following commands: 54 | 55 | .. code:: python 56 | 57 | import micropython 58 | micropython.mem_info() 59 | 60 | 61 | Supported devices 62 | ================= 63 | 64 | pyprofibus has been tested on: 65 | 66 | * ESP32 WROOM32 67 | 68 | pyprofibus probably runs on other devices, too. The major limiting factor is the memory available to pyprofibus. 69 | 70 | 71 | main.py 72 | ======= 73 | 74 | Please see the file `main.py` and edit it to your needs. 75 | 76 | It is the main file that will be executed after boot. It should start your application and pyprofibus. 77 | 78 | 79 | boot.py 80 | ======= 81 | 82 | The script `boot.py` sets up a basic environment after Micropython boot. You probably don't need to edit the default `boot.py` script shipped with pyprofibus. 83 | -------------------------------------------------------------------------------- /micropython/boot.py: -------------------------------------------------------------------------------- 1 | print("boot.py") 2 | 3 | # Start the watchdog first. 4 | if 1: 5 | import machine 6 | watchdog = machine.WDT(timeout=5000).feed 7 | print("Watchdog active.") 8 | else: 9 | watchdog = None 10 | print("Watchdog inactive.") 11 | 12 | import sys, gc 13 | 14 | # Add to Python path. 15 | sys.path.insert(0, "/stublibs") 16 | sys.path.append("/examples") 17 | sys.path.append("/misc") 18 | 19 | # Enable gc after allocation of this many bytes: 20 | gc.threshold(2**11) 21 | gc.collect() 22 | -------------------------------------------------------------------------------- /micropython/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Build pyprofibus and install on a Micropython board. 4 | # 5 | 6 | basedir="$(dirname "$0")" 7 | [ "$(echo "$basedir" | cut -c1)" = '/' ] || basedir="$PWD/$basedir" 8 | 9 | rootdir="$(realpath -m "$basedir/..")" 10 | 11 | die() 12 | { 13 | echo "ERROR: $*" >&2 14 | exit 1 15 | } 16 | 17 | echos() 18 | { 19 | printf '%s' "$*" 20 | } 21 | 22 | build() 23 | { 24 | echo "=== build ===" 25 | 26 | cd "$rootdir" || die "Failed to switch to root dir." 27 | 28 | local gsds="$(find . -maxdepth 1 -name '*.gsd') $(find misc/ -maxdepth 1 -name '*.gsd')" 29 | local pys="$(find pyprofibus/ -name '*.py') $(find stublibs/ -name '*.py') $(find examples -maxdepth 1 -name 'example_*.py')" 30 | local confs="$(find examples -maxdepth 1 -name '*.conf')" 31 | 32 | local gsdparser_opts="--dump-strip --dump-notext --dump-noextuserprmdata \ 33 | --dump-module '6ES7 138-4CA01-0AA0 PM-E DC24V' \ 34 | --dump-module '6ES7 132-4BB30-0AA0 2DO DC24V' \ 35 | --dump-module '6ES7 132-4BB30-0AA0 2DO DC24V' \ 36 | --dump-module '6ES7 131-4BD01-0AA0 4DI DC24V' \ 37 | --dump-module 'Master_O Slave_I 1 by unit' \ 38 | --dump-module 'Master_I Slave_O 1 by unit' \ 39 | --dump-module 'dummy output module' \ 40 | --dump-module 'dummy input module' \ 41 | $modules" 42 | 43 | local targets= 44 | [ -n "$clean" ] && local targets="$targets clean" 45 | local targets="$targets all" 46 | for target in $targets; do 47 | echo "--- $target ---" 48 | make -j "$(getconf _NPROCESSORS_ONLN)" -f "$rootdir/micropython/Makefile" \ 49 | SRCDIR="." \ 50 | MAINPYDIR="./micropython" \ 51 | BUILDDIR="$builddir" \ 52 | MPYCROSS="$mpycross" \ 53 | MARCH="$march" \ 54 | GSDPARSER_OPTS="$gsdparser_opts" \ 55 | PYS="$pys" \ 56 | GSDS="$gsds" \ 57 | CONFS="$confs" \ 58 | $target || die "make failed." 59 | done 60 | } 61 | 62 | pyboard() 63 | { 64 | echo "$pyboard -d $dev $*" 65 | "$pyboard" -d "$dev" "$@" || die "pyboard: $pyboard failed." 66 | } 67 | 68 | reboot_dev() 69 | { 70 | pyboard --no-follow -c 'import machine as m;m.reset()' 71 | } 72 | 73 | format_flash() 74 | { 75 | local wd_timeout="5000" 76 | 77 | local cmd="import machine as m, flashbdev as f, uos as u;" 78 | cmd="${cmd}m.WDT(0,${wd_timeout}).feed();" 79 | cmd="${cmd}u.umount('/');" 80 | cmd="${cmd}u.VfsLfs2.mkfs(f.bdev);" 81 | cmd="${cmd}u.mount(u.VfsLfs2(f.bdev), '/');" 82 | pyboard -c "$cmd" 83 | } 84 | 85 | transfer() 86 | { 87 | local from="$1" 88 | local to="$2" 89 | 90 | if [ -d "$from" ]; then 91 | pyboard -f mkdir "$to" 92 | for f in "$from"/*; do 93 | transfer "$f" "$to/$(basename "$f")" 94 | done 95 | return 96 | fi 97 | pyboard -f cp "$from" "$to" 98 | } 99 | 100 | transfer_to_device() 101 | { 102 | echo "=== transfer to device $dev ===" 103 | 104 | format_flash 105 | reboot_dev 106 | sleep 2 107 | for f in "$builddir"/*; do 108 | transfer "$f" :/"$(basename $f)" 109 | done 110 | reboot_dev 111 | } 112 | 113 | builddir="$rootdir/build/micropython" 114 | buildonly=0 115 | dev="/dev/ttyUSB0" 116 | march="xtensawin" 117 | pyboard="pyboard.py" 118 | mpycross="mpy-cross" 119 | modules= 120 | clean= 121 | 122 | while [ $# -ge 1 ]; do 123 | [ "$(echos "$1" | cut -c1)" != "-" ] && break 124 | 125 | case "$1" in 126 | -h|--help) 127 | echo "install.sh [OPTIONS] [TARGET-UART-DEVICE]" 128 | echo 129 | echo "Build pyprofibus and install on a Micropython board." 130 | echo 131 | echo "TARGET-UART-DEVICE:" 132 | echo " Target serial device. Default: /dev/ttyUSB0" 133 | echo 134 | echo "Options:" 135 | echo " -c|--clean Clean before build." 136 | echo " -b|--build-only Build only. Do not install." 137 | echo " -B|--build-dir DIR Set the build directory." 138 | echo " Default: build/micropython" 139 | echo " -a|--march ARCH Target architecture for cross compile." 140 | echo " Default: xtensawin" 141 | echo " -m|--module NAME Include GSD module NAME." 142 | echo " Can be specified multiple times for multiple modules." 143 | echo " Enter your 'module_X' names from your configuration here." 144 | echo " -M|--mpycross PATH Path to mpy-cross executable." 145 | echo " Default: mpy-cross" 146 | echo " -p|--pyboard PATH Path to pyboard executable." 147 | echo " Default: pyboard.py" 148 | echo " -h|--help Show this help." 149 | exit 0 150 | ;; 151 | -b|--build-only) 152 | buildonly=1 153 | ;; 154 | -B|--build-dir) 155 | shift 156 | builddir="$1" 157 | ;; 158 | -a|--march) 159 | shift 160 | march="$1" 161 | ;; 162 | -p|--pyboard) 163 | shift 164 | pyboard="$1" 165 | ;; 166 | -M|--mpycross) 167 | shift 168 | mpycross="$1" 169 | ;; 170 | -m|--module) 171 | shift 172 | modules="$modules --dump-module '$1'" 173 | ;; 174 | -c|--clean) 175 | clean=1 176 | ;; 177 | *) 178 | die "Unknown option: $1" 179 | ;; 180 | esac 181 | shift 182 | done 183 | if [ $# -ge 1 ]; then 184 | dev="$1" 185 | shift 186 | fi 187 | if [ $# -ge 1 ]; then 188 | die "Too many arguments." 189 | fi 190 | 191 | build 192 | [ $buildonly -eq 0 ] && transfer_to_device 193 | exit 0 194 | -------------------------------------------------------------------------------- /micropython/main.py: -------------------------------------------------------------------------------- 1 | print("main.py") 2 | import machine, gc, sys 3 | 4 | # Start the pyprofibus application. 5 | # Modify this to your needs. 6 | # This function should never return. If it does nevertheless, a reboot is attempted. 7 | def start_pyprofibus(): 8 | print("Starting pyprofibus...") 9 | 10 | # Start dummy example that uses virtual bus hardware. 11 | import example_dummy_oneslave 12 | example_dummy_oneslave.main(confdir="examples", watchdog=watchdog) # Run main loop 13 | return 14 | 15 | # Start the S7-312-2DP example. 16 | #import example_s7_315_2dp 17 | #example_s7_315_2dp.main(confdir="examples", watchdog=watchdog) # Run main loop 18 | #return 19 | 20 | # Start the ET200S example. 21 | #import example_et200s 22 | #example_et200s.main(confdir="examples", watchdog=watchdog) # Run main loop 23 | #return 24 | 25 | 26 | # Main execution loop. 27 | # This runs start_pyprofibus and does its best to catch exceptions. 28 | count = 0 29 | while True: 30 | try: 31 | count += 1 32 | gc.collect() 33 | start_pyprofibus() 34 | except KeyboardInterrupt as e: 35 | raise e 36 | except Exception as e: 37 | try: 38 | print("FATAL exception:") 39 | sys.print_exception(e) 40 | except: pass 41 | except: pass 42 | try: 43 | if count >= 5: 44 | count = 0 45 | machine.reset() 46 | except: pass 47 | -------------------------------------------------------------------------------- /misc/dummy_compact.gsd: -------------------------------------------------------------------------------- 1 | ; 2 | ; pyprofibus - Dummy GSD file 3 | ; 4 | 5 | #Profibus_DP 6 | 7 | GSD_Revision=1 8 | Slave_Family=3@Digital@24V 9 | Vendor_Name="PYPROFIBUS" 10 | Model_Name="PYPROFIBUS DUMMY" 11 | OrderNumber="42-42-42" 12 | Revision="42" 13 | Hardware_Release="42" 14 | Software_Release="42" 15 | Ident_Number=0x4224 16 | Protocol_Ident=0 17 | Station_Type=0 18 | Fail_Safe=1 19 | DPV1_Slave=1 20 | 21 | 22 | PrmText=1 23 | Text(0)="disabled" 24 | Text(1)="enabled" 25 | EndPrmText 26 | 27 | ExtUserPrmData=1 "dummy feature 1" 28 | Bit(2) 0 0-1 29 | Prm_Text_Ref=1 30 | EndExtUserPrmData 31 | 32 | ExtUserPrmData=2 "dummy feature 2" 33 | Bit(3) 0 0-1 34 | Prm_Text_Ref=1 35 | EndExtUserPrmData 36 | 37 | 38 | Auto_Baud_supp=1 39 | 9.6_supp=1 40 | 19.2_supp=1 41 | 45.45_supp=1 42 | 93.75_supp=1 43 | 187.5_supp=1 44 | 500_supp=1 45 | 1.5M_supp=1 46 | 3M_supp=1 47 | 6M_supp=1 48 | 12M_supp=1 49 | MaxTsdr_9.6=60 50 | MaxTsdr_19.2=60 51 | MaxTsdr_45.45=250 52 | MaxTsdr_93.75=60 53 | MaxTsdr_187.5=60 54 | MaxTsdr_500=100 55 | MaxTsdr_1.5M=150 56 | MaxTsdr_3M=250 57 | MaxTsdr_6M=450 58 | MaxTsdr_12M=800 59 | 60 | Min_Slave_Intervall=10 61 | 62 | Max_Diag_Data_Len=128 63 | 64 | User_Prm_Data_Len=4 65 | User_Prm_Data = 0x00,0x00,0x00,0x42 66 | Max_User_Prm_Data_Len=16 67 | Ext_User_Prm_Data_Const(0)=0x00,0x00,0x00,0x42 68 | Ext_User_Prm_Data_Ref(3)=1 69 | Ext_User_Prm_Data_Ref(3)=2 70 | 71 | 72 | Modular_Station=0 73 | Modul_Offset=1 74 | 75 | Module="nop module" 0x00 76 | EndModule 77 | 78 | Module="dummy input module" 0x10 79 | EndModule 80 | 81 | Module="dummy output module" 0x20 82 | EndModule 83 | -------------------------------------------------------------------------------- /misc/dummy_modular.gsd: -------------------------------------------------------------------------------- 1 | ; 2 | ; pyprofibus - Dummy GSD file 3 | ; 4 | 5 | #Profibus_DP 6 | 7 | GSD_Revision=1 8 | Slave_Family=3@Digital@24V 9 | Vendor_Name="PYPROFIBUS" 10 | Model_Name="PYPROFIBUS DUMMY" 11 | OrderNumber="42-42-42" 12 | Revision="42" 13 | Hardware_Release="42" 14 | Software_Release="42" 15 | Ident_Number=0x4224 16 | Protocol_Ident=0 17 | Station_Type=0 18 | Fail_Safe=1 19 | DPV1_Slave=1 20 | 21 | 22 | PrmText=1 23 | Text(0)="disabled" 24 | Text(1)="enabled" 25 | EndPrmText 26 | 27 | ExtUserPrmData=1 "dummy feature 1" 28 | Bit(2) 0 0-1 29 | Prm_Text_Ref=1 30 | EndExtUserPrmData 31 | 32 | ExtUserPrmData=2 "dummy feature 2" 33 | Bit(3) 0 0-1 34 | Prm_Text_Ref=1 35 | EndExtUserPrmData 36 | 37 | 38 | Auto_Baud_supp=1 39 | 9.6_supp=1 40 | 19.2_supp=1 41 | 45.45_supp=1 42 | 93.75_supp=1 43 | 187.5_supp=1 44 | 500_supp=1 45 | 1.5M_supp=1 46 | 3M_supp=1 47 | 6M_supp=1 48 | 12M_supp=1 49 | MaxTsdr_9.6=60 50 | MaxTsdr_19.2=60 51 | MaxTsdr_45.45=250 52 | MaxTsdr_93.75=60 53 | MaxTsdr_187.5=60 54 | MaxTsdr_500=100 55 | MaxTsdr_1.5M=150 56 | MaxTsdr_3M=250 57 | MaxTsdr_6M=450 58 | MaxTsdr_12M=800 59 | 60 | Min_Slave_Intervall=10 61 | 62 | Max_Diag_Data_Len=128 63 | 64 | User_Prm_Data_Len=4 65 | User_Prm_Data = 0x00,0x00,0x00,0x42 66 | Max_User_Prm_Data_Len=16 67 | Ext_User_Prm_Data_Const(0)=0x00,0x00,0x00,0x42 68 | Ext_User_Prm_Data_Ref(3)=1 69 | Ext_User_Prm_Data_Ref(3)=2 70 | 71 | 72 | Modular_Station=1 73 | Modul_Offset=1 74 | Max_Module=32 75 | FixPresetModules=1 76 | Max_Input_Len=249 77 | Max_Output_Len=249 78 | Max_Data_Len=498 79 | 80 | Module="fixed module" 0x00 81 | Preset=1 82 | EndModule 83 | 84 | Module="dummy input module" 0x10 85 | EndModule 86 | 87 | Module="dummy output module" 0x20 88 | EndModule 89 | -------------------------------------------------------------------------------- /phy_fpga/.gitignore: -------------------------------------------------------------------------------- 1 | *.blif 2 | *.json 3 | *.asc 4 | *.bin 5 | *.rpt 6 | *.log 7 | *.stamp 8 | crc8_func.v 9 | pll_mod.v 10 | -------------------------------------------------------------------------------- /phy_fpga/Makefile: -------------------------------------------------------------------------------- 1 | # Project name 2 | NAME := pyprofibusphy 3 | 4 | # Target board configuration 5 | TARGET := tinyfpga_bx 6 | PLL_HZ := 24000000 7 | 8 | DEBUG := 0 9 | 10 | # Source files 11 | TOP_FILE := main.v 12 | TOP_MODULE := top_module 13 | PCF_FILE := $(TARGET).pcf 14 | 15 | # Generated files 16 | PLL_MOD_V_FILE := pll_mod.v 17 | GENERATED_V := crc8_func.v 18 | 19 | # Extra dependencies 20 | EXTRA_DEP_V := 21 | EXTRA_DEP_PY := 22 | 23 | # Additional cleanup 24 | CLEAN_FILES := crcgen.stamp 25 | 26 | include fpgamakelib/fpgamakelib.mk 27 | 28 | crcgen.stamp: 29 | $(TEST) -f ./crcgen/crcgen || $(GIT) submodule update --init 30 | $(TOUCH) $@ 31 | 32 | crc8_func.v: crcgen.stamp 33 | PYTHONPATH=./crcgen $(PYTHON) ./crcgen/crcgen --algorithm CRC-8-CCITT --verilog-function --name crc8 > $@ 34 | -------------------------------------------------------------------------------- /phy_fpga/block_ram_mod.v: -------------------------------------------------------------------------------- 1 | // vim: ts=4 sw=4 noexpandtab 2 | /* 3 | * Block RAM 4 | * 5 | * Copyright (c) 2019 Michael Buesch 6 | * 7 | * This program is free software; you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation; either version 2 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License along 18 | * with this program; if not, write to the Free Software Foundation, Inc., 19 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 20 | */ 21 | 22 | `ifndef BLOCK_RAM_MOD_V_ 23 | `define BLOCK_RAM_MOD_V_ 24 | 25 | 26 | module block_ram #( 27 | parameter ADDR_WIDTH = 16, 28 | parameter DATA_WIDTH = 8, 29 | parameter MEM_BYTES = 1024, 30 | ) ( 31 | input clk, 32 | /* Port 0 */ 33 | input [ADDR_WIDTH - 1 : 0] addr0, 34 | output reg [DATA_WIDTH - 1 : 0] rd_data0, 35 | input [DATA_WIDTH - 1 : 0] wr_data0, 36 | input wr0, 37 | /* Port 1 */ 38 | input [ADDR_WIDTH - 1 : 0] addr1, 39 | output reg [DATA_WIDTH - 1 : 0] rd_data1, 40 | ); 41 | reg [DATA_WIDTH - 1 : 0] mem [MEM_BYTES - 1 : 0]; 42 | integer i; 43 | 44 | initial begin 45 | for (i = 0; i < MEM_BYTES; i++) begin 46 | mem[i] <= 0; 47 | end 48 | end 49 | 50 | always @(posedge clk) begin 51 | if (wr0) begin 52 | mem[addr0] <= wr_data0; 53 | end 54 | rd_data0 <= mem[addr0]; 55 | rd_data1 <= mem[addr1]; 56 | end 57 | endmodule 58 | 59 | 60 | `endif /* BLOCK_RAM_MOD_V_ */ 61 | -------------------------------------------------------------------------------- /phy_fpga/edge_detect_mod.v: -------------------------------------------------------------------------------- 1 | // vim: ts=4 sw=4 noexpandtab 2 | /* 3 | * Edge detection 4 | * 5 | * Copyright (c) 2019 Michael Buesch 6 | * 7 | * This program is free software; you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation; either version 2 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License along 18 | * with this program; if not, write to the Free Software Foundation, Inc., 19 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 20 | */ 21 | 22 | `ifndef EDGE_DETECT_MOD_V_ 23 | `define EDGE_DETECT_MOD_V_ 24 | 25 | 26 | module edge_detect #( 27 | parameter NR_BITS = 1, /* Number of bits */ 28 | ) ( 29 | input wire clk, /* Clock */ 30 | input wire [NR_BITS - 1 : 0] signal, /* Input signal */ 31 | output wire [NR_BITS - 1 : 0] rising, /* Rising edge detected on input signal */ 32 | output wire [NR_BITS - 1 : 0] falling, /* Falling edge detected on input signal */ 33 | ); 34 | reg [NR_BITS - 1 : 0] prev_signal; 35 | 36 | always @(posedge clk) begin 37 | prev_signal <= signal; 38 | end 39 | 40 | assign rising = ~prev_signal & signal; 41 | assign falling = prev_signal & ~signal; 42 | endmodule 43 | 44 | 45 | `endif /* EDGE_DETECT_MOD_V_ */ 46 | -------------------------------------------------------------------------------- /phy_fpga/led_blink_mod.v: -------------------------------------------------------------------------------- 1 | // vim: ts=4 sw=4 noexpandtab 2 | /* 3 | * LED blinker 4 | * 5 | * Copyright (c) 2019 Michael Buesch 6 | * 7 | * This program is free software; you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation; either version 2 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License along 18 | * with this program; if not, write to the Free Software Foundation, Inc., 19 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 20 | */ 21 | 22 | `ifndef LED_BLINK_MOD_V_ 23 | `define LED_BLINK_MOD_V_ 24 | 25 | 26 | module led_blink #( 27 | parameter BLINK_ON_CLKS = 1024, 28 | parameter BLINK_OFF_CLKS = 1024, 29 | ) ( 30 | input clk, 31 | input n_reset, 32 | input enable, 33 | output reg led, 34 | ); 35 | reg [31:0] led_on_count; 36 | reg [31:0] led_off_count; 37 | 38 | initial begin 39 | led <= 0; 40 | led_on_count <= 0; 41 | led_off_count <= 0; 42 | end 43 | 44 | always @(posedge clk) begin 45 | if (n_reset) begin 46 | if (led) begin 47 | if (led_on_count == 0) begin 48 | led <= 0; 49 | led_off_count <= BLINK_OFF_CLKS; 50 | end else begin 51 | led_on_count <= led_on_count - 1; 52 | end 53 | end else begin 54 | if (led_off_count == 0) begin 55 | if (enable) begin 56 | led <= 1; 57 | led_on_count <= BLINK_ON_CLKS; 58 | end 59 | end else begin 60 | led_off_count <= led_off_count - 1; 61 | end 62 | end 63 | end else begin 64 | led <= 0; 65 | led_on_count <= 0; 66 | led_off_count <= 0; 67 | end 68 | end 69 | endmodule 70 | 71 | `endif /* LED_BLINK_MOD_V_ */ 72 | -------------------------------------------------------------------------------- /phy_fpga/main.v: -------------------------------------------------------------------------------- 1 | // vim: ts=4 sw=4 noexpandtab 2 | /* 3 | * pyprofibus FPGA PHY 4 | * 5 | * Copyright (c) 2019 Michael Buesch 6 | * 7 | * This program is free software; you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation; either version 2 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License along 18 | * with this program; if not, write to the Free Software Foundation, Inc., 19 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 20 | */ 21 | 22 | `include "profibus_phy_mod.v" 23 | `include "led_blink_mod.v" 24 | 25 | 26 | `ifdef DEBUG 27 | `define DEBUGOUT output 28 | `else 29 | `define DEBUGOUT input 30 | `endif 31 | 32 | 33 | module common_main_module #( 34 | parameter CLK_HZ = 0, 35 | ) ( 36 | input clk, 37 | input n_reset, 38 | 39 | /* SPI bus */ 40 | input spi_mosi, 41 | inout spi_miso, 42 | input spi_sck, 43 | input spi_ss, 44 | 45 | /* Profibus and status */ 46 | input pb_rx, 47 | output pb_rx_error, 48 | output pb_rx_irq_edge, 49 | output pb_rx_irq_level, 50 | output pb_tx, 51 | output pb_tx_active, 52 | output pb_tx_error, 53 | 54 | /* Status and debugging */ 55 | output led, 56 | `ifdef DEBUG 57 | output debug, 58 | `endif 59 | ); 60 | wire miso; 61 | wire sck; 62 | wire ss; 63 | wire rx_error; 64 | wire rx_irq_edge; 65 | wire rx_irq_level; 66 | wire tx; 67 | wire tx_error; 68 | wire rx_active; 69 | wire tx_active; 70 | `ifdef DEBUG 71 | wire debug_w; 72 | `endif 73 | 74 | profibus_phy pb( 75 | .clk(clk), 76 | .n_reset(n_reset), 77 | .rx_irq_edge(rx_irq_edge), 78 | .rx_irq_level(rx_irq_level), 79 | .mosi(spi_mosi), 80 | .miso(miso), 81 | .sck(spi_sck), 82 | .ss(spi_ss), 83 | .rx(pb_rx), 84 | .rx_active(rx_active), 85 | .rx_error(rx_error), 86 | .tx(tx), 87 | .tx_active(tx_active), 88 | .tx_error(tx_error), 89 | `ifdef DEBUG 90 | .debug(debug_w), 91 | `endif 92 | ); 93 | 94 | bufif0(spi_miso, miso, spi_ss); 95 | bufif0(pb_rx_error, rx_error, 0); 96 | bufif0(pb_rx_irq_edge, rx_irq_edge, 0); 97 | bufif0(pb_rx_irq_level, rx_irq_level, 0); 98 | bufif0(pb_tx, tx, 0); 99 | bufif0(pb_tx_active, tx_active, 0); 100 | bufif0(pb_tx_error, tx_error, 0); 101 | `ifdef DEBUG 102 | bufif0(debug, debug_w, 0); 103 | `endif 104 | 105 | wire led_w; 106 | wire led_enable; 107 | assign led_enable = tx_active | rx_active; 108 | 109 | led_blink #( 110 | .BLINK_ON_CLKS(CLK_HZ / 10), 111 | .BLINK_OFF_CLKS(CLK_HZ / 35), 112 | ) led_blink ( 113 | .clk(clk), 114 | .n_reset(n_reset), 115 | .enable(led_enable), 116 | .led(led_w), 117 | ); 118 | bufif0(led, led_w, 0); 119 | endmodule 120 | 121 | 122 | `ifdef TARGET_TINYFPGA_BX 123 | 124 | `include "pll_mod.v" 125 | 126 | /* TinyFPGA BX: 127 | * +---------------+ 128 | * |P|GND Vin|P| 129 | * not reset |O|1 GND|P| 130 | * debug |D|2 3.3V|P| 131 | * |N|3 T 24|N| 132 | * |N|4 i 23|N| 133 | * |N|5 n 22|N| 134 | * |N|6 y 21|N| 135 | * |N|7 F 20|O| PB RX IRQ level 136 | * |N|8 P 19|O| PB RX IRQ edge 137 | * |N|9 G 18|O| PB TX error 138 | * SPI MISO |O|10 A 17|O| PB RX error 139 | * SPI MOSI |I|11 16|O| PB TX active 140 | * SPI SCK |I|12 B 15|O| PB UART TX 141 | * SPI SS |I|13 X 14|I| PB UART RX 142 | * +---------------+ 143 | * P = power 144 | * I = input 145 | * O = output 146 | * D = debug output. Only if DEBUG is enabled. Otherwise N. 147 | * N = not connected 148 | */ 149 | module top_module( 150 | input CLK, 151 | input SPI_SS, 152 | input SPI_SCK, 153 | input SPI_IO0, 154 | input SPI_IO1, 155 | input SPI_IO2, 156 | input SPI_IO3, 157 | input USBP, 158 | input USBN, 159 | output USBPU, 160 | output LED, 161 | input PIN_1, 162 | `DEBUGOUT PIN_2, 163 | input PIN_3, 164 | input PIN_4, 165 | input PIN_5, 166 | input PIN_6, 167 | input PIN_7, 168 | input PIN_8, 169 | input PIN_9, 170 | inout PIN_10, 171 | input PIN_11, 172 | input PIN_12, 173 | input PIN_13, 174 | input PIN_14, 175 | output PIN_15, 176 | output PIN_16, 177 | output PIN_17, 178 | output PIN_18, 179 | output PIN_19, 180 | output PIN_20, 181 | input PIN_21, 182 | input PIN_22, 183 | input PIN_23, 184 | input PIN_24, 185 | input PIN_25, 186 | input PIN_26, 187 | input PIN_27, 188 | input PIN_28, 189 | input PIN_29, 190 | input PIN_30, 191 | input PIN_31, 192 | ); 193 | 194 | wire pll_clk_out; 195 | wire pll_locked; 196 | 197 | pll_module pll( 198 | .clock_in(CLK), 199 | .clock_out(pll_clk_out), 200 | .locked(pll_locked), 201 | ); 202 | 203 | wire n_reset; 204 | assign n_reset = PIN_1 & pll_locked; 205 | 206 | common_main_module #( 207 | .CLK_HZ(`PLL_HZ), 208 | ) common ( 209 | .clk(pll_clk_out), 210 | .n_reset(n_reset), 211 | .spi_mosi(PIN_11), 212 | .spi_miso(PIN_10), 213 | .spi_sck(PIN_12), 214 | .spi_ss(PIN_13), 215 | .pb_rx(PIN_14), 216 | .pb_rx_error(PIN_17), 217 | .pb_rx_irq_edge(PIN_19), 218 | .pb_rx_irq_level(PIN_20), 219 | .pb_tx(PIN_15), 220 | .pb_tx_active(PIN_16), 221 | .pb_tx_error(PIN_18), 222 | .led(LED), 223 | `ifdef DEBUG 224 | .debug(PIN_2), 225 | `endif 226 | ); 227 | 228 | assign USBPU = 0; /* Disable USB */ 229 | endmodule 230 | 231 | `else /* TARGET */ 232 | `ERROR____TARGET_is_not_known 233 | `endif /* TARGET */ 234 | -------------------------------------------------------------------------------- /phy_fpga/parity_func.v: -------------------------------------------------------------------------------- 1 | // vim: ts=4 sw=4 noexpandtab 2 | /* 3 | * Parity calculation 4 | * 5 | * Copyright (c) 2019 Michael Buesch 6 | * 7 | * This program is free software; you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation; either version 2 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License along 18 | * with this program; if not, write to the Free Software Foundation, Inc., 19 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 20 | */ 21 | 22 | 23 | localparam EVEN = 0; 24 | localparam ODD = 1; 25 | 26 | function automatic parity9; 27 | input odd; 28 | input a; 29 | input b; 30 | input c; 31 | input d; 32 | input e; 33 | input f; 34 | input g; 35 | input h; 36 | input i; 37 | 38 | begin 39 | parity9 = odd ^ a ^ b ^ c ^ d ^ e ^ f ^ g ^ h ^ i; 40 | end 41 | endfunction 42 | 43 | function automatic parity8; 44 | input odd; 45 | input a; 46 | input b; 47 | input c; 48 | input d; 49 | input e; 50 | input f; 51 | input g; 52 | input h; 53 | 54 | begin 55 | parity8 = parity9(odd, a, b, c, d, e, f, g, h, 0); 56 | end 57 | endfunction 58 | -------------------------------------------------------------------------------- /phy_fpga/spi_slave_mod.v: -------------------------------------------------------------------------------- 1 | // vim: ts=4 sw=4 noexpandtab 2 | /* 3 | * SPI bus slave 4 | * 5 | * Copyright (c) 2019 Michael Buesch 6 | * 7 | * This program is free software; you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation; either version 2 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License along 18 | * with this program; if not, write to the Free Software Foundation, Inc., 19 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 20 | */ 21 | 22 | `ifndef SPI_SLAVE_MOD_V_ 23 | `define SPI_SLAVE_MOD_V_ 24 | 25 | `include "sync_signal_mod.v" 26 | 27 | 28 | module spi_slave #( 29 | parameter WORDSIZE = 8, /* Size of SPI word. Can be anything from 1 to 32. */ 30 | parameter CPOL = 0, /* SPI clock polarity. Can be 0 or 1. */ 31 | parameter CPHA = 0, /* SPI clock phase. Can be 0 or 1. */ 32 | parameter MSB_FIRST = 1, /* MSB transmit first enable. Can be 0 or 1. */ 33 | ) ( 34 | input clk, /* clock */ 35 | input mosi, /* SPI bus MOSI signal */ 36 | output miso, /* SPI bus MISO signal */ 37 | input sck, /* SPI bus clock signal */ 38 | input ss, /* SPI bus slave select signal */ 39 | output rx_irq, /* Receive interrupt */ 40 | output [WORDSIZE - 1 : 0] rx_data, /* Received data */ 41 | input [WORDSIZE - 1 : 0] tx_data, /* Transmit data */ 42 | ); 43 | /* Synchronized input signals. */ 44 | wire mosi_s; 45 | wire sck_rising_s; 46 | wire sck_falling_s; 47 | wire ss_s; 48 | sync_signal sync_mosi(.clk(clk), .in(mosi), .out(mosi_s)); 49 | sync_signal sync_sck(.clk(clk), .in(sck), .rising(sck_rising_s), .falling(sck_falling_s)); 50 | sync_signal sync_ss(.clk(clk), .in(ss), .out(ss_s)); 51 | 52 | /* SCK sample and setup edges. */ 53 | wire sck_sample_edge; 54 | wire sck_setup_edge; 55 | assign sck_sample_edge = (CPOL ^ CPHA) ? sck_falling_s : sck_rising_s; 56 | assign sck_setup_edge = (CPOL ^ CPHA) ? sck_rising_s : sck_falling_s; 57 | 58 | /* Output buffers. */ 59 | reg miso_r; 60 | reg rx_irq_r; 61 | reg [WORDSIZE - 1 : 0] rx_data_r; 62 | assign miso = miso_r; 63 | assign rx_irq = rx_irq_r; 64 | assign rx_data = rx_data_r; 65 | 66 | /* Receive and transmit shift registers. */ 67 | reg [WORDSIZE - 1 : 0] rx_shiftreg; 68 | reg [WORDSIZE - 1 : 0] tx_shiftreg; 69 | reg [5:0] bit_count; 70 | 71 | initial begin 72 | bit_count <= 0; 73 | rx_shiftreg <= 0; 74 | tx_shiftreg <= 0; 75 | 76 | miso_r <= 0; 77 | 78 | rx_irq_r <= 0; 79 | rx_data_r <= 0; 80 | end 81 | 82 | always @(posedge clk) begin 83 | /* Check if slave select is not active */ 84 | if (ss_s) begin 85 | bit_count <= 0; 86 | rx_shiftreg <= 0; 87 | tx_shiftreg <= 0; 88 | 89 | miso_r <= 0; 90 | rx_irq_r <= 0; 91 | 92 | /* Check if slave select is active */ 93 | end else begin 94 | 95 | /* Check if we are at the start of a word. */ 96 | if (bit_count == 0) begin 97 | if (CPHA) begin 98 | /* Reload the TX shift register. */ 99 | tx_shiftreg <= tx_data; 100 | miso_r <= 0; 101 | end else begin 102 | /* Reload the TX shift register and 103 | * put the first bit onto the bus. */ 104 | if (MSB_FIRST) begin 105 | tx_shiftreg <= tx_data << 1; 106 | miso_r <= tx_data[WORDSIZE - 1]; 107 | end else begin 108 | tx_shiftreg <= tx_data >> 1; 109 | miso_r <= tx_data[0]; 110 | end 111 | end 112 | /* Check if we are at a setup edge of SCK. */ 113 | end else if (sck_setup_edge) begin 114 | /* Put the next bit onto the bus. */ 115 | if (MSB_FIRST) begin 116 | miso_r <= tx_shiftreg[WORDSIZE - 1]; 117 | tx_shiftreg <= tx_shiftreg << 1; 118 | end else begin 119 | miso_r <= tx_shiftreg[0]; 120 | tx_shiftreg <= tx_shiftreg >> 1; 121 | end 122 | end 123 | 124 | /* Check if we are at a sample edge of SCK. */ 125 | if (sck_sample_edge && (bit_count < WORDSIZE)) begin 126 | /* Get the next bit from the bus. */ 127 | if (MSB_FIRST) begin 128 | rx_shiftreg <= rx_shiftreg << 1; 129 | rx_shiftreg[0] <= mosi_s; 130 | end else begin 131 | rx_shiftreg <= rx_shiftreg >> 1; 132 | rx_shiftreg[WORDSIZE - 1] <= mosi_s; 133 | end 134 | bit_count <= bit_count + 1; 135 | end 136 | 137 | /* If we received a full word, trigger the RX interrupt. */ 138 | if (bit_count >= WORDSIZE) begin 139 | bit_count <= 0; 140 | rx_data_r <= rx_shiftreg; 141 | rx_irq_r <= 1; 142 | end else begin 143 | rx_irq_r <= 0; 144 | end 145 | end 146 | end 147 | endmodule 148 | 149 | `endif /* SPI_SLAVE_MOD_V_ */ 150 | -------------------------------------------------------------------------------- /phy_fpga/sync_signal_mod.v: -------------------------------------------------------------------------------- 1 | // vim: ts=4 sw=4 noexpandtab 2 | /* 3 | * Synchronize a signal to a clock 4 | * 5 | * Copyright (c) 2019 Michael Buesch 6 | * 7 | * This program is free software; you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation; either version 2 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License along 18 | * with this program; if not, write to the Free Software Foundation, Inc., 19 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 20 | */ 21 | 22 | `ifndef SYNC_SIGNAL_MOD_V_ 23 | `define SYNC_SIGNAL_MOD_V_ 24 | 25 | module sync_signal( 26 | input clk, /* clock */ 27 | input in, /* input signal */ 28 | output out, /* synchronized output signal */ 29 | output falling, /* synchronized falling edge output */ 30 | output rising, /* synchronized rising edge output */ 31 | ); 32 | reg [2:0] shiftreg; 33 | 34 | initial begin 35 | shiftreg <= 0; 36 | end 37 | 38 | always @(posedge clk) begin 39 | shiftreg[2:1] <= shiftreg[1:0]; 40 | shiftreg[0] <= in; 41 | end 42 | 43 | assign out = shiftreg[1]; 44 | assign falling = shiftreg[2] & ~shiftreg[1]; 45 | assign rising = ~shiftreg[2] & shiftreg[1]; 46 | 47 | endmodule 48 | 49 | `endif /* SYNC_SIGNAL_MOD_V_ */ 50 | -------------------------------------------------------------------------------- /phy_fpga/tinyfpga_bx.pcf: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # TinyFPGA BX constraint file (.pcf) 4 | # 5 | ############################################################################### 6 | # 7 | # Copyright (c) 2018, Luke Valenty 8 | # All rights reserved. 9 | # 10 | # Redistribution and use in source and binary forms, with or without 11 | # modification, are permitted provided that the following conditions are met: 12 | # 13 | # 1. Redistributions of source code must retain the above copyright notice, this 14 | # list of conditions and the following disclaimer. 15 | # 2. Redistributions in binary form must reproduce the above copyright notice, 16 | # this list of conditions and the following disclaimer in the documentation 17 | # and/or other materials provided with the distribution. 18 | # 19 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 23 | # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 26 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | # 30 | # The views and conclusions contained in the software and documentation are those 31 | # of the authors and should not be interpreted as representing official policies, 32 | # either expressed or implied, of the project. 33 | # 34 | ############################################################################### 35 | 36 | #### 37 | # TinyFPGA BX information: https://github.com/tinyfpga/TinyFPGA-BX/ 38 | #### 39 | 40 | # Left side of board 41 | set_io --warn-no-port PIN_1 A2 42 | set_io --warn-no-port PIN_2 A1 43 | set_io --warn-no-port PIN_3 B1 44 | set_io --warn-no-port PIN_4 C2 45 | set_io --warn-no-port PIN_5 C1 46 | set_io --warn-no-port PIN_6 D2 47 | set_io --warn-no-port PIN_7 D1 48 | set_io --warn-no-port PIN_8 E2 49 | set_io --warn-no-port PIN_9 E1 50 | set_io --warn-no-port PIN_10 G2 51 | set_io --warn-no-port PIN_11 H1 52 | set_io --warn-no-port PIN_12 J1 53 | set_io --warn-no-port PIN_13 H2 54 | 55 | # Right side of board 56 | set_io --warn-no-port PIN_14 H9 57 | set_io --warn-no-port PIN_15 D9 58 | set_io --warn-no-port PIN_16 D8 59 | set_io --warn-no-port PIN_17 C9 60 | set_io --warn-no-port PIN_18 A9 61 | set_io --warn-no-port PIN_19 B8 62 | set_io --warn-no-port PIN_20 A8 63 | set_io --warn-no-port PIN_21 B7 64 | set_io --warn-no-port PIN_22 A7 65 | set_io --warn-no-port PIN_23 B6 66 | set_io --warn-no-port PIN_24 A6 67 | 68 | # SPI flash interface on bottom of board 69 | set_io --warn-no-port SPI_SS F7 70 | set_io --warn-no-port SPI_SCK G7 71 | set_io --warn-no-port SPI_IO0 G6 72 | set_io --warn-no-port SPI_IO1 H7 73 | set_io --warn-no-port SPI_IO2 H4 74 | set_io --warn-no-port SPI_IO3 J8 75 | 76 | # General purpose pins on bottom of board 77 | set_io --warn-no-port PIN_25 G1 78 | set_io --warn-no-port PIN_26 J3 79 | set_io --warn-no-port PIN_27 J4 80 | set_io --warn-no-port PIN_28 G9 81 | set_io --warn-no-port PIN_29 J9 82 | set_io --warn-no-port PIN_30 E8 83 | set_io --warn-no-port PIN_31 J2 84 | 85 | # LED 86 | set_io --warn-no-port LED B3 87 | 88 | # USB 89 | set_io --warn-no-port USBP B4 90 | set_io --warn-no-port USBN A4 91 | set_io --warn-no-port USBPU A3 92 | 93 | # 16MHz clock 94 | set_io --warn-no-port CLK B2 # input 95 | -------------------------------------------------------------------------------- /phy_fpga/tinyfpga_bx_program.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | tinyprog --update-bootloader 4 | tinyprog --program tinyfpga_bx_pyprofibusphy.bin 5 | -------------------------------------------------------------------------------- /profisniff: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | # 4 | # PROFIBUS - Telegram sniffer 5 | # 6 | # Copyright (c) 2016-2020 Michael Buesch 7 | # 8 | # Licensed under the terms of the GNU General Public License version 2, 9 | # or (at your option) any later version. 10 | # 11 | """ 12 | 13 | from pyprofibus.phy_serial import * 14 | from pyprofibus.fdl import * 15 | from pyprofibus import * 16 | 17 | import sys 18 | import getopt 19 | 20 | 21 | def usage(): 22 | print("PROFIBUS bus sniffer") 23 | print("") 24 | print("Usage: profisniff [OPTIONS] DEVICE") 25 | print("") 26 | print("DEVICE is the PHY device /dev/ttySx") 27 | print("") 28 | print("Options:") 29 | print(" -h|--help Show this help.") 30 | 31 | def main(): 32 | try: 33 | (opts, args) = getopt.getopt(sys.argv[1:], 34 | "h", 35 | [ "help", ]) 36 | except getopt.GetoptError as e: 37 | sys.stderr.write(str(e) + "\n") 38 | usage() 39 | return 1 40 | for (o, v) in opts: 41 | if o in ("-h", "--help"): 42 | usage() 43 | return 0 44 | if len(args) != 1: 45 | usage() 46 | return 1 47 | dev = args[0] 48 | 49 | try: 50 | phy = CpPhySerial(dev) 51 | xceiv = FdlTransceiver(phy) 52 | xceiv.setRXFilter(None) 53 | except ProfibusError as e: 54 | sys.stderr.write("ERROR: %s\n" % str(e)) 55 | return 1 56 | try: 57 | while True: 58 | try: 59 | ok, telegram = xceiv.poll(-1.0) 60 | if not ok or not telegram: 61 | continue 62 | print(telegram) 63 | except ProfibusError as e: 64 | sys.stderr.write("ERROR: %s\n" % str(e)) 65 | except KeyboardInterrupt: 66 | return 0 67 | return 1 68 | 69 | if __name__ == "__main__": 70 | sys.exit(main()) 71 | -------------------------------------------------------------------------------- /pyprofibus/__init__.py: -------------------------------------------------------------------------------- 1 | from pyprofibus.conf import PbConf, PbConfError 2 | from pyprofibus.dp import DpError 3 | from pyprofibus.dp_master import DPM1, DPM2 4 | from pyprofibus.fdl import FdlError 5 | from pyprofibus.phy import PhyError 6 | from pyprofibus.util import ProfibusError 7 | -------------------------------------------------------------------------------- /pyprofibus/compat.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Python compatibility 4 | # 5 | # Copyright 2012-2020 Michael Buesch 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation; either version 2 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License along 18 | # with this program; if not, write to the Free Software Foundation, Inc., 19 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 20 | # 21 | 22 | from __future__ import division, absolute_import, print_function, unicode_literals 23 | 24 | import sys 25 | 26 | __all__ = [ 27 | "isPy3Compat", 28 | "isPy2Compat", 29 | "isMicropython", 30 | "IOError", 31 | ] 32 | 33 | # isPy3Compat is True, if the interpreter is Python 3 compatible. 34 | isPy3Compat = sys.version_info[0] == 3 35 | 36 | # isPy2Compat is True, if the interpreter is Python 2 compatible. 37 | isPy2Compat = sys.version_info[0] == 2 38 | 39 | # isMicroPython is True, if the interpreter is Micropython 40 | isMicropython = hasattr(sys, "implementation") and sys.implementation.name == "micropython" 41 | 42 | # IOError dummy 43 | try: 44 | IOError = IOError 45 | except NameError: 46 | IOError = OSError 47 | -------------------------------------------------------------------------------- /pyprofibus/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # PROFIBUS DP - Configuration file parser 4 | # 5 | # Copyright (c) 2016-2021 Michael Buesch 6 | # 7 | # Licensed under the terms of the GNU General Public License version 2, 8 | # or (at your option) any later version. 9 | # 10 | 11 | from __future__ import division, absolute_import, print_function, unicode_literals 12 | from pyprofibus.compat import * 13 | 14 | from pyprofibus.gsd.interp import GsdInterp 15 | from pyprofibus.gsd.parser import GsdError 16 | from pyprofibus.util import * 17 | 18 | import re 19 | import sys 20 | from io import StringIO 21 | 22 | if isPy2Compat: 23 | from ConfigParser import SafeConfigParser as _ConfigParser 24 | from ConfigParser import Error as _ConfigParserError 25 | else: 26 | from configparser import ConfigParser as _ConfigParser 27 | from configparser import Error as _ConfigParserError 28 | 29 | __all__ = [ 30 | "PbConfError", 31 | "PbConf", 32 | ] 33 | 34 | def loadGsd(name, debug=0): 35 | exceptions = [] 36 | # Try name as a direct file name. 37 | try: 38 | return GsdInterp.fromFile(name, debug=(debug > 0)) 39 | except GsdError as e: 40 | exceptions.append(str(e)) 41 | # Try to interpret the name as a module name. 42 | try: 43 | if "/" not in name: 44 | mod = name.replace(".gsd", "_gsd") 45 | return GsdInterp.fromPy(mod, debug=(debug > 0)) 46 | except GsdError as e: 47 | exceptions.append(str(e)) 48 | # Try to interpret the name as a module name with leading fs path components stripped. 49 | try: 50 | slashIdx = name.rfind("/") 51 | if slashIdx >= 0: 52 | mod = name[slashIdx + 1 : ].replace(".gsd", "_gsd") 53 | return GsdInterp.fromPy(mod, debug=(debug > 0)) 54 | except GsdError as e: 55 | exceptions.append(str(e)) 56 | raise GsdError("\n".join(exceptions)) 57 | 58 | class PbConfError(ProfibusError): 59 | pass 60 | 61 | class PbConf(object): 62 | """Pyprofibus configuration file parser. 63 | """ 64 | 65 | class _SlaveConf(object): 66 | """Slave configuration. 67 | """ 68 | index = None 69 | name = None 70 | addr = None 71 | gsd = None 72 | syncMode = None 73 | freezeMode = None 74 | groupMask = None 75 | watchdogMs = None 76 | inputSize = None 77 | outputSize = None 78 | diagPeriod = None 79 | 80 | def makeDpSlaveDesc(self): 81 | """Create a DpSlaveDesc instance based on the configuration. 82 | """ 83 | from pyprofibus.dp_master import DpSlaveDesc 84 | slaveDesc = DpSlaveDesc(self) 85 | 86 | # Create Chk_Cfg telegram 87 | slaveDesc.setCfgDataElements(self.gsd.getCfgDataElements()) 88 | 89 | # Set User_Prm_Data 90 | slaveDesc.setUserPrmData(self.gsd.getUserPrmData()) 91 | 92 | # Set various standard parameters 93 | slaveDesc.setSyncMode(self.syncMode) 94 | slaveDesc.setFreezeMode(self.freezeMode) 95 | slaveDesc.setGroupMask(self.groupMask) 96 | slaveDesc.setWatchdog(self.watchdogMs) 97 | 98 | return slaveDesc 99 | 100 | # [PROFIBUS] section 101 | debug = None 102 | # [PHY] section 103 | phyType = None 104 | phyDev = None 105 | phyBaud = None 106 | phyRtsCts = None 107 | phyDsrDtr = None 108 | phySpiBus = None 109 | phySpiCS = None 110 | phySpiSpeedHz = None 111 | # [DP] section 112 | dpMasterClass = None 113 | dpMasterAddr = None 114 | # [SLAVE_xxx] sections 115 | slaveConfs = None 116 | 117 | @classmethod 118 | def fromFile(cls, filename): 119 | if isPy2Compat: 120 | with open(filename, "r") as fd: 121 | return cls(fd, filename) 122 | else: 123 | with open(filename, "r", encoding="UTF-8") as fd: 124 | return cls(fd, filename) 125 | 126 | __reSlave = re.compile(r'^SLAVE_(\d+)$') 127 | __reMod = re.compile(r'^module_(\d+)$') 128 | 129 | def __init__(self, fd, filename=None): 130 | def get(section, option, fallback = None): 131 | if p.has_option(section, option): 132 | return p.get(section, option) 133 | if fallback is None: 134 | raise ValueError("Option [%s] '%s' does not exist." % ( 135 | section, option)) 136 | return fallback 137 | def getboolean(section, option, fallback = None): 138 | if p.has_option(section, option): 139 | return p.getboolean(section, option) 140 | if fallback is None: 141 | raise ValueError("Option [%s] '%s' does not exist." % ( 142 | section, option)) 143 | return fallback 144 | def getint(section, option, fallback = None): 145 | if p.has_option(section, option): 146 | return p.getint(section, option) 147 | if fallback is None: 148 | raise ValueError("Option [%s] '%s' does not exist." % ( 149 | section, option)) 150 | return fallback 151 | try: 152 | p = _ConfigParser() 153 | if hasattr(p, "read_file"): 154 | p.read_file(fd, filename) 155 | else: 156 | p.readfp(fd, filename) 157 | 158 | # [PROFIBUS] 159 | self.debug = getint("PROFIBUS", "debug", 160 | fallback=0) 161 | 162 | # [PHY] 163 | self.phyType = get("PHY", "type", 164 | fallback="serial") 165 | self.phyDev = get("PHY", "dev", 166 | fallback="/dev/ttyS0") 167 | self.phyBaud = getint("PHY", "baud", 168 | fallback=9600) 169 | self.phyRtsCts = getboolean("PHY", "rtscts", 170 | fallback=False) 171 | self.phyDsrDtr = getboolean("PHY", "dsrdtr", 172 | fallback=False) 173 | self.phySpiBus = getint("PHY", "spiBus", 174 | fallback=0) 175 | self.phySpiCS = getint("PHY", "spiCS", 176 | fallback=0) 177 | self.phySpiSpeedHz = getint("PHY", "spiSpeedHz", 178 | fallback=1000000) 179 | 180 | # [DP] 181 | self.dpMasterClass = getint("DP", "master_class", 182 | fallback=1) 183 | if self.dpMasterClass not in {1, 2}: 184 | raise ValueError("Invalid master_class") 185 | self.dpMasterAddr = getint("DP", "master_addr", 186 | fallback=0x02) 187 | if self.dpMasterAddr < 0 or self.dpMasterAddr > 127: 188 | raise ValueError("Invalid master_addr") 189 | 190 | self.slaveConfs = [] 191 | for section in p.sections(): 192 | m = self.__reSlave.match(section) 193 | if not m: 194 | continue 195 | index = int(m.group(1)) 196 | s = self._SlaveConf() 197 | s.index = index 198 | s.name = get(section, "name", section) 199 | s.addr = getint(section, "addr") 200 | s.gsd = loadGsd(get(section, "gsd"), self.debug) 201 | s.syncMode = getboolean(section, "sync_mode", 202 | fallback=False) 203 | s.freezeMode = getboolean(section, "freeze_mode", 204 | fallback=False) 205 | s.groupMask = getboolean(section, "group_mask", 206 | fallback=1) 207 | if s.groupMask < 0 or s.groupMask > 0xFF: 208 | raise ValueError("Invalid group_mask") 209 | s.watchdogMs = getint(section, "watchdog_ms", 210 | fallback=5000) 211 | if s.watchdogMs < 0 or s.watchdogMs > 255 * 255: 212 | raise ValueError("Invalid watchdog_ms") 213 | s.inputSize = getint(section, "input_size") 214 | if s.inputSize < 0 or s.inputSize > 246: 215 | raise ValueError("Invalid input_size") 216 | s.outputSize = getint(section, "output_size") 217 | if s.outputSize < 0 or s.outputSize > 246: 218 | raise ValueError("Invalid output_size") 219 | s.diagPeriod = getint(section, "diag_period", 0) 220 | if s.diagPeriod < 0 or s.diagPeriod > 0x3FFFFFFF: 221 | raise ValueError("Invalid diag_period") 222 | 223 | mods = [ o for o in p.options(section) 224 | if self.__reMod.match(o) ] 225 | mods.sort(key = lambda o: int(self.__reMod.match(o).group(1))) 226 | if s.gsd.isModular(): 227 | for option in mods: 228 | s.gsd.setConfiguredModule(get(section, option)) 229 | elif mods: 230 | print("Warning: Some modules are specified in the config file, " 231 | "but the station is 'Compact': Modular_Station=0.", 232 | file=sys.stderr) 233 | 234 | self.slaveConfs.append(s) 235 | 236 | except (IOError, UnicodeError) as e: 237 | raise PbConfError("Failed to read '%s': %s" %\ 238 | (filename, str(e))) 239 | except (_ConfigParserError, ValueError) as e: 240 | raise PbConfError("Profibus config file parse " 241 | "error:\n%s" % str(e)) 242 | except GsdError as e: 243 | raise PbConfError("Failed to parse GSD file:\n%s" % str(e)) 244 | 245 | def makePhy(self): 246 | """Create a CP-PHY instance based on the configuration. 247 | """ 248 | phyType = self.phyType.lower().strip() 249 | if phyType == "serial": 250 | import pyprofibus.phy_serial 251 | phyClass = pyprofibus.phy_serial.CpPhySerial 252 | extraKwArgs = {} 253 | elif phyType in {"dummyslave", "dummy_slave", "dummy-slave"}: 254 | import pyprofibus.phy_dummy 255 | phyClass = pyprofibus.phy_dummy.CpPhyDummySlave 256 | extraKwArgs = { 257 | "echoDX" : all(slaveConf.outputSize > 0 258 | for slaveConf in self.slaveConfs), 259 | "echoDXSize" : max(slaveConf.outputSize 260 | for slaveConf in self.slaveConfs), 261 | } 262 | elif phyType == "fpga": 263 | import pyprofibus.phy_fpga 264 | phyClass = pyprofibus.phy_fpga.CpPhyFPGA 265 | extraKwArgs = {} 266 | else: 267 | raise PbConfError("Invalid phyType parameter value: " 268 | "%s" % self.phyType) 269 | phy = phyClass(debug=(self.debug >= 2), 270 | port=self.phyDev, 271 | spiBus=self.phySpiBus, 272 | spiCS=self.phySpiCS, 273 | spiSpeedHz=self.phySpiSpeedHz, 274 | **extraKwArgs) 275 | phy.setConfig(baudrate=self.phyBaud, 276 | rtscts=self.phyRtsCts, 277 | dsrdtr=self.phyDsrDtr) 278 | return phy 279 | 280 | def makeDPM(self, phy=None): 281 | """Create a DpMaster and a CP-PHY instance based on the configuration. 282 | Returns the DpMaster instance. 283 | """ 284 | if phy is None: 285 | # Create a PHY (layer 1) interface object. 286 | phy = self.makePhy() 287 | 288 | # Create a DP class 1 or 2 master. 289 | from pyprofibus.dp_master import DPM1, DPM2 290 | if self.dpMasterClass == 1: 291 | DpMasterClass = DPM1 292 | elif self.dpMasterClass == 2: 293 | DpMasterClass = DPM2 294 | else: 295 | raise PbConfError("Invalid dpMasterClass parameter value: " 296 | "%d" % self.dpMasterClass) 297 | master = DpMasterClass(phy=phy, 298 | masterAddr=self.dpMasterAddr, 299 | debug=(self.debug >= 1)) 300 | return master 301 | -------------------------------------------------------------------------------- /pyprofibus/gsd/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, absolute_import, print_function, unicode_literals 2 | 3 | from pyprofibus.gsd.interp import * 4 | from pyprofibus.gsd.parser import * 5 | -------------------------------------------------------------------------------- /pyprofibus/gsd/fields.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # PROFIBUS - GSD file parser 4 | # 5 | # Copyright (c) 2016-2020 Michael Buesch 6 | # 7 | # Licensed under the terms of the GNU General Public License version 2, 8 | # or (at your option) any later version. 9 | # 10 | 11 | import re 12 | 13 | __all__ = [ 14 | "GSD_STR", 15 | "gsdrepr", 16 | "PrmText", 17 | "PrmTextValue", 18 | "ExtUserPrmData", 19 | "ExtUserPrmDataConst", 20 | "ExtUserPrmDataRef", 21 | "Module", 22 | ] 23 | 24 | GSD_STR = ( 25 | "Prm_Text_Ref", 26 | "Ext_Module_Prm_Data_Len", 27 | "ExtUserPrmData", 28 | "Ext_User_Prm_Data_Const", 29 | "Ext_User_Prm_Data_Ref", 30 | ) 31 | 32 | def gsdrepr(x): 33 | if isinstance(x, list): 34 | return "[%s]" % ", ".join(gsdrepr(a) for a in x) 35 | if isinstance(x, tuple): 36 | return "(%s, )" % ", ".join(gsdrepr(a) for a in x) 37 | if isinstance(x, dict): 38 | return "{%s}" % ", ".join("%s : %s\n" % (gsdrepr(a), gsdrepr(b)) 39 | for a, b in x.items()) 40 | if isinstance(x, str): 41 | for i, cs in enumerate(GSD_STR): 42 | if cs == x: 43 | return "GSD_STR[%s]" % i 44 | return repr(x) 45 | 46 | class _Item(object): 47 | """Abstract item base class. 48 | """ 49 | 50 | __slots__ = ( 51 | "fields", 52 | ) 53 | 54 | def __init__(self, fields=None): 55 | self.fields = fields or {} 56 | 57 | def getField(self, name, default=None): 58 | """Get a field by name. 59 | """ 60 | return self.fields.get(name, default) 61 | 62 | def _repr_field(self, pfx=", ", sfx=""): 63 | if self.fields: 64 | return "%sfields=%s%s" % (pfx, gsdrepr(self.fields), sfx) 65 | return "" 66 | 67 | class PrmText(_Item): 68 | """PrmText section. 69 | """ 70 | 71 | REPR_NAME = "PrmText" 72 | 73 | __slots__ = ( 74 | "refNr", 75 | "texts", 76 | ) 77 | 78 | def __init__(self, refNr, texts=None, **kwargs): 79 | _Item.__init__(self, **kwargs) 80 | self.refNr = refNr 81 | self.texts = texts or [] 82 | 83 | def __repr__(self): 84 | return "%s(%s, %s%s)" % ( 85 | self.REPR_NAME, 86 | gsdrepr(self.refNr), 87 | gsdrepr(self.texts), 88 | self._repr_field()) 89 | 90 | class PrmTextValue(_Item): 91 | """PrmText text value. 92 | """ 93 | 94 | REPR_NAME = "PrmTextValue" 95 | 96 | __slots__ = ( 97 | "offset", 98 | "text", 99 | ) 100 | 101 | def __init__(self, offset, text, **kwargs): 102 | _Item.__init__(self, **kwargs) 103 | self.offset = offset 104 | self.text = text 105 | 106 | def __repr__(self): 107 | return "%s(%s, %s%s)" % ( 108 | self.REPR_NAME, 109 | gsdrepr(self.offset), 110 | gsdrepr(self.text), 111 | self._repr_field()) 112 | 113 | class ExtUserPrmData(_Item): 114 | """ExtUserPrmData section. 115 | """ 116 | 117 | REPR_NAME = "ExtUserPrmData" 118 | 119 | __slots__ = ( 120 | "refNr", 121 | "name", 122 | ) 123 | 124 | def __init__(self, refNr, name, **kwargs): 125 | _Item.__init__(self, **kwargs) 126 | self.refNr = refNr 127 | self.name = name 128 | 129 | def __repr__(self): 130 | return "%s(%s, %s%s)" % ( 131 | self.REPR_NAME, 132 | gsdrepr(self.refNr), 133 | gsdrepr(self.name), 134 | self._repr_field()) 135 | 136 | class ExtUserPrmDataConst(_Item): 137 | """Ext_User_Prm_Data_Const(x) 138 | """ 139 | 140 | REPR_NAME = "ExtUserPrmDataConst" 141 | 142 | __slots__ = ( 143 | "offset", 144 | "dataBytes", 145 | ) 146 | 147 | def __init__(self, offset, dataBytes, **kwargs): 148 | _Item.__init__(self, **kwargs) 149 | self.offset = offset 150 | self.dataBytes = dataBytes 151 | 152 | def __repr__(self): 153 | return "%s(%s, %s%s)" % ( 154 | self.REPR_NAME, 155 | gsdrepr(self.offset), 156 | gsdrepr(self.dataBytes), 157 | self._repr_field()) 158 | 159 | class ExtUserPrmDataRef(_Item): 160 | """Ext_User_Prm_Data_Ref(x) 161 | """ 162 | 163 | REPR_NAME = "ExtUserPrmDataRef" 164 | 165 | __slots__ = ( 166 | "offset", 167 | "refNr", 168 | ) 169 | 170 | def __init__(self, offset, refNr, **kwargs): 171 | _Item.__init__(self, **kwargs) 172 | self.offset = offset 173 | self.refNr = refNr 174 | 175 | def __repr__(self): 176 | return "%s(%s, %s%s)" % ( 177 | self.REPR_NAME, 178 | gsdrepr(self.offset), 179 | gsdrepr(self.refNr), 180 | self._repr_field()) 181 | 182 | class Module(_Item): 183 | """Module section. 184 | """ 185 | 186 | REPR_NAME = "Module" 187 | 188 | __slots__ = ( 189 | "name", 190 | "configBytes", 191 | ) 192 | 193 | @classmethod 194 | def sanitizeName(cls, name): 195 | return re.subn(r"\s+", " ", name)[0].strip() 196 | 197 | def __init__(self, name, configBytes, **kwargs): 198 | _Item.__init__(self, **kwargs) 199 | self.name = name 200 | self.configBytes = configBytes 201 | 202 | def __repr__(self): 203 | return "%s(%s, %s%s)" % ( 204 | self.REPR_NAME, 205 | gsdrepr(self.name), 206 | gsdrepr(self.configBytes), 207 | self._repr_field()) 208 | -------------------------------------------------------------------------------- /pyprofibus/gsd/interp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # PROFIBUS - GSD file interpreter 4 | # 5 | # Copyright (c) 2016-2021 Michael Buesch 6 | # 7 | # Licensed under the terms of the GNU General Public License version 2, 8 | # or (at your option) any later version. 9 | # 10 | 11 | from __future__ import division, absolute_import, print_function, unicode_literals 12 | from pyprofibus.compat import * 13 | 14 | from pyprofibus.gsd.parser import GsdParser, GsdError 15 | from pyprofibus.dp import DpCfgDataElement 16 | 17 | import difflib 18 | 19 | __all__ = [ 20 | "GsdInterp", 21 | ] 22 | 23 | class GsdInterp(GsdParser): 24 | """GSD file/data interpreter. 25 | """ 26 | 27 | def __init__(self, text, filename=None, debug=False): 28 | super(GsdInterp, self).__init__(text, filename, debug) 29 | self.__configMods = [] 30 | self.__addPresetModules(onlyFixed=False) 31 | if not self.isModular(): 32 | self.__addAllModules() 33 | 34 | def __interpErr(self, errorText): 35 | raise GsdError("GSD '%s': %s" % ( 36 | self.getFileName() or "", 37 | errorText)) 38 | 39 | def __interpWarn(self, errorText): 40 | if not self.debugEnabled(): 41 | return 42 | print("Warning in GSD '%s': %s" % ( 43 | self.getFileName() or "", 44 | errorText)) 45 | 46 | @staticmethod 47 | def __findInSequence(sequence, findName, getItemName): 48 | if not sequence: 49 | return None 50 | nameLower = findName.lower().strip() 51 | 52 | # Check if there's only one matching exactly. 53 | matches = [ item for item in sequence 54 | if findName == getItemName(item) ] 55 | if len(matches) == 1: 56 | return matches[0] 57 | 58 | # Check if there's only one matching exactly (case insensitive). 59 | matches = [ item for item in sequence 60 | if nameLower == getItemName(item).lower().strip() ] 61 | if len(matches) == 1: 62 | return matches[0] 63 | 64 | # Check if there's only one matching at the start. 65 | matches = [ item for item in sequence 66 | if getItemName(item).lower().strip().startswith(nameLower) ] 67 | if len(matches) == 1: 68 | return matches[0] 69 | 70 | # Fuzzy match. 71 | matches = difflib.get_close_matches( 72 | findName, 73 | ( getItemName(item) for item in sequence ), 74 | n = 1) 75 | if matches: 76 | matches = [ item for item in sequence 77 | if getItemName(item) == matches[0] ] 78 | if matches: 79 | return matches[0] 80 | return None 81 | 82 | def findModule(self, name): 83 | """Find a module by name. 84 | Returns a _Module instance, if found. None otherwise. 85 | """ 86 | return self.__findInSequence(self.getField("Module"), 87 | name, 88 | lambda module: module.name) 89 | 90 | def __addPresetModules(self, onlyFixed=False): 91 | if not self.getField("FixPresetModules", False) and\ 92 | onlyFixed: 93 | return 94 | for mod in self.getField("Module", []): 95 | if mod.getField("Preset", False): 96 | self.__configMods.append(mod) 97 | 98 | def __addAllModules(self): 99 | """Configure all available modules as plugged into the device. 100 | """ 101 | for mod in self.getField("Module", []): 102 | if not mod.getField("Preset", False): 103 | self.__configMods.append(mod) 104 | 105 | def clearConfiguredModules(self): 106 | """Remove all configured modules. 107 | This also removes all preset modules, except for the fixed preset mods. 108 | """ 109 | self.__configMods = [] 110 | self.__addPresetModules(onlyFixed=True) 111 | 112 | def setConfiguredModule(self, moduleName, index=-1): 113 | """Set a configured module that is plugged into the device. 114 | If index>=0 then set the module at the specified index. 115 | If index<0 then append the module. 116 | If moduleName is None then the module is removed. 117 | """ 118 | if index >= 0 and index < len(self.__configMods) and\ 119 | self.getField("FixPresetModules", False) and\ 120 | self.__configMods[index].getField("Preset", False): 121 | self.__interpErr("Not modifying fixed preset module " 122 | "at index %d." % index) 123 | if moduleName is None: 124 | if index >= 0 and index < len(self.__configMods): 125 | self.__configMods.pop(index) 126 | else: 127 | self.__interpErr("Module index %d out of range." % ( 128 | index)) 129 | else: 130 | mod = self.findModule(moduleName) 131 | if not mod: 132 | self.__interpErr("Module '%s' not found in GSD." % ( 133 | moduleName)) 134 | if index < 0 or index >= len(self.__configMods): 135 | self.__configMods.append(mod) 136 | else: 137 | self.__configMods[index] = mod 138 | 139 | def isModular(self): 140 | """Returns True, if this is a modular device. 141 | """ 142 | return self.getField("Modular_Station", False) 143 | 144 | def isDPV1(self): 145 | """Returns True, if this is a DPV1 slave. 146 | """ 147 | return self.getField("DPV1_Slave", False) 148 | 149 | def getCfgDataElements(self): 150 | """Get a tuple of config data elements (DpCfgDataElement) 151 | for this station with the configured modules. 152 | """ 153 | elems = [] 154 | for mod in self.__configMods: 155 | elems.append(DpCfgDataElement( 156 | mod.configBytes[0], 157 | mod.configBytes[1:])) 158 | return elems 159 | 160 | def getUserPrmData(self, dp1PrmMask = None, dp1PrmSet = None): 161 | """Get a bytearray of User_Prm_Data 162 | for this station with the configured modules. 163 | dp1PrmMask/Set: Optional mask/set override for the DPV1 prm. 164 | """ 165 | def merge(baseData, extData, offset): 166 | if extData is not None: 167 | baseData[offset : len(extData) + offset] = extData 168 | def trunc(data, length, fieldname, extend = True): 169 | if length is not None: 170 | if extend: 171 | data.extend(bytearray(max(length - len(data), 0))) 172 | if len(data) > length: 173 | self.__interpWarn("User_Prm_Data " 174 | "truncated by %s" % fieldname) 175 | data[:] = data[0:length] 176 | # Get the global data. 177 | data = bytearray(self.getField("User_Prm_Data", b"")) 178 | trunc(data, self.getField("User_Prm_Data_Len"), 179 | "User_Prm_Data_Len") 180 | for dataConst in self.getField("Ext_User_Prm_Data_Const", []): 181 | merge(data, dataConst.dataBytes, dataConst.offset) 182 | # Append the module parameter data. 183 | for mod in self.__configMods: 184 | modData = bytearray() 185 | for dataConst in mod.getField("Ext_User_Prm_Data_Const", []): 186 | merge(modData, dataConst.dataBytes, dataConst.offset) 187 | trunc(modData, mod.getField("Ext_Module_Prm_Data_Len"), 188 | "Ext_Module_Prm_Data_Len") 189 | # Add to global data. 190 | data += modData 191 | if self.isDPV1(): 192 | assert((dp1PrmMask is None and dp1PrmSet is None) or\ 193 | (dp1PrmMask is not None and dp1PrmSet is not None)) 194 | if dp1PrmMask is not None: 195 | assert(len(dp1PrmMask) == 3 and len(dp1PrmSet) == 3) 196 | if len(data) < 3: 197 | self.__interpErr("DPv1 User_Prm_Data is " 198 | "shorter than 3 bytes.") 199 | # Apply the DPv1 prm override. 200 | for i in range(3): 201 | data[i] = (data[i] & ~dp1PrmMask[i]) |\ 202 | (dp1PrmSet[i] & dp1PrmMask[i]) 203 | elif dp1PrmMask is not None: 204 | self.__interpWarn("DPv1 User_Prm_Data override ignored") 205 | trunc(data, self.getField("Max_User_Prm_Data_Len"), 206 | "Max_User_Prm_Data_Len", False) 207 | return bytes(data) 208 | 209 | def getIdentNumber(self): 210 | """Get the ident number. 211 | """ 212 | ident = self.getField("Ident_Number") 213 | if ident is None: 214 | self.__interpErr("No Ident_Number in GSD.") 215 | return ident 216 | 217 | def getMaxTSDR(self, baudrate): 218 | """Get the max-tSDR. 219 | Might return None. 220 | """ 221 | baud2fieldname = { 222 | 9600 : "MaxTsdr_9.6", 223 | 19200 : "MaxTsdr_19.2", 224 | 45450 : "MaxTsdr_45.45", 225 | 93750 : "MaxTsdr_93.75", 226 | 187500 : "MaxTsdr_187.5", 227 | 500000 : "MaxTsdr_500", 228 | 1500000 : "MaxTsdr_1.5M", 229 | 3000000 : "MaxTsdr_3M", 230 | 6000000 : "MaxTsdr_6M", 231 | 12000000 : "MaxTsdr_12M", 232 | } 233 | if baudrate not in baud2fieldname: 234 | self.__interpErr("getMaxTSDR: Invalid baud rate.") 235 | fieldname = baud2fieldname[baudrate] 236 | return self.getField(fieldname, None) 237 | 238 | def __str__(self): 239 | text = [] 240 | 241 | if self.getFileName(): 242 | text.append("File: %s\n" %\ 243 | self.getFileName()) 244 | vendor = self.getField("Vendor_Name", "") 245 | model = self.getField("Model_Name", "") 246 | rev = self.getField("Revision", "") 247 | ident = self.getField("Ident_Number") 248 | text.append("Device: %s; %s; %s; Ident %s\n" % ( 249 | vendor, model, rev, 250 | ("0x%04X" % ident) if ident is not None else "-")) 251 | 252 | order = self.getField("OrderNumber") 253 | if order: 254 | text.append("Order number: %s\n" % order) 255 | 256 | for module in self.getField("Module", []): 257 | if module.getField("Preset"): 258 | continue 259 | text.append("Available module: \"%s\"\n" % module.name) 260 | 261 | return "".join(text) 262 | -------------------------------------------------------------------------------- /pyprofibus/phy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # PROFIBUS DP - Communication Processor PHY access library 4 | # 5 | # Copyright (c) 2013-2021 Michael Buesch 6 | # 7 | # Licensed under the terms of the GNU General Public License version 2, 8 | # or (at your option) any later version. 9 | # 10 | 11 | from __future__ import division, absolute_import, print_function, unicode_literals 12 | from pyprofibus.compat import * 13 | 14 | import time 15 | import sys 16 | from collections import deque 17 | 18 | from pyprofibus.util import * 19 | 20 | 21 | __all__ = [ 22 | "PhyError", 23 | "CpPhy", 24 | ] 25 | 26 | 27 | class PhyError(ProfibusError): 28 | """PHY exception. 29 | """ 30 | 31 | class CpPhy(object): 32 | """PROFIBUS CP PHYsical layer base class. 33 | """ 34 | 35 | PFX = "PHY: " 36 | 37 | # Profibus baud-rates 38 | BAUD_9600 = 9600 39 | BAUD_19200 = 19200 40 | BAUD_45450 = 45450 41 | BAUD_93750 = 93750 42 | BAUD_187500 = 187500 43 | BAUD_500000 = 500000 44 | BAUD_1500000 = 1500000 45 | BAUD_3000000 = 3000000 46 | BAUD_6000000 = 6000000 47 | BAUD_12000000 = 12000000 48 | 49 | __slots__ = ( 50 | "debug", 51 | "__txQueueDAs", 52 | "__txQueueTelegrams", 53 | "__allocUntil", 54 | "__secPerFrame", 55 | ) 56 | 57 | def __init__(self, debug=False, *args, **kwargs): 58 | self.debug = debug 59 | self.__close() 60 | 61 | def _debugMsg(self, msg): 62 | if self.debug: 63 | print(self.PFX + str(msg)) 64 | 65 | def _warningMsg(self, msg): 66 | print("%sWarning: %s" % (self.PFX, str(msg))) 67 | 68 | def close(self): 69 | """Close the PHY device. 70 | This method may be reimplemented in the PHY driver. 71 | """ 72 | self.__close() 73 | 74 | def __close(self): 75 | self.__txQueueDAs = deque() 76 | self.__txQueueTelegrams = [None] * (0x7F + 1) 77 | self.__allocUntil = monotonic_time() 78 | self.__secPerFrame = 0.0 79 | 80 | def sendData(self, telegramData, srd): 81 | """Send data to the physical line. 82 | Reimplement this method in the PHY driver. 83 | """ 84 | raise NotImplementedError 85 | 86 | def pollData(self, timeout): 87 | """Poll received data from the physical line. 88 | timeout => timeout in seconds. 89 | 0.0 = no timeout, return immediately. 90 | negative = unlimited. 91 | Reimplement this method in the PHY driver. 92 | """ 93 | raise NotImplementedError 94 | 95 | def poll(self, timeout=0.0): 96 | """timeout => timeout in seconds. 97 | 0.0 = no timeout, return immediately. 98 | negative = unlimited. 99 | """ 100 | if self.__txQueueDAs: 101 | self.__send() 102 | return self.pollData(timeout) 103 | 104 | def __send(self): 105 | now = monotonic_time() 106 | if self.__canAllocateBus(now): 107 | da = self.__txQueueDAs.popleft() 108 | telegram, srd, maxReplyLen = self.__txQueueTelegrams[da] 109 | self.__txQueueTelegrams[da] = None 110 | telegramData = telegram.getRawData() 111 | self.__allocateBus(now, len(telegramData), maxReplyLen) 112 | self.sendData(telegramData, srd) 113 | 114 | def send(self, telegram, srd, maxReplyLen=-1): 115 | if maxReplyLen < 0 or maxReplyLen > 255: 116 | maxReplyLen = 255 117 | 118 | da = telegram.da 119 | if self.__txQueueTelegrams[da] is None: 120 | self.__txQueueDAs.append(da) 121 | self.__txQueueTelegrams[da] = (telegram, srd, maxReplyLen) 122 | 123 | self.__send() 124 | 125 | def setConfig(self, baudrate=BAUD_9600, *args, **kwargs): 126 | """Set the PHY configuration. 127 | This method may be reimplemented in the PHY driver. 128 | """ 129 | symLen = 1.0 / baudrate 130 | self.__secPerFrame = symLen * float(1 + 8 + 1 + 1) 131 | 132 | def __canAllocateBus(self, now): 133 | return now >= self.__allocUntil 134 | 135 | def __allocateBus(self, now, nrSendOctets, nrReplyOctets): 136 | secPerFrame = self.__secPerFrame 137 | seconds = secPerFrame * nrSendOctets 138 | if nrReplyOctets: 139 | pass#TODO IFS 140 | seconds += secPerFrame * nrReplyOctets 141 | pass#TODO 142 | self.__allocUntil = now + seconds 143 | 144 | def releaseBus(self): 145 | self.__allocUntil = monotonic_time() 146 | if self.__txQueueDAs: 147 | self.__send() 148 | 149 | def clearTxQueueAddr(self, da): 150 | """Remove all TX queue entries for the given destination address. 151 | """ 152 | if da in self.__txQueueDAs: 153 | self.__txQueueDAs.remove(da) 154 | self.__txQueueTelegrams[da] = None 155 | -------------------------------------------------------------------------------- /pyprofibus/phy_dummy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # PROFIBUS DP - Communication Processor PHY access library 4 | # 5 | # Copyright (c) 2016-2021 Michael Buesch 6 | # 7 | # Licensed under the terms of the GNU General Public License version 2, 8 | # or (at your option) any later version. 9 | # 10 | 11 | from __future__ import division, absolute_import, print_function, unicode_literals 12 | from pyprofibus.compat import * 13 | 14 | from pyprofibus.phy import * 15 | from pyprofibus.fdl import * 16 | from pyprofibus.dp import * 17 | from pyprofibus.util import * 18 | 19 | __all__ = [ 20 | "CpPhyDummySlave", 21 | ] 22 | 23 | class CpPhyDummySlave(CpPhy): 24 | """Dummy slave PROFIBUS CP PHYsical layer 25 | """ 26 | 27 | __slots__ = ( 28 | "__echoDX", 29 | "__echoDXSize", 30 | "__pollQueue", 31 | ) 32 | 33 | def __init__(self, *args, **kwargs): 34 | super(CpPhyDummySlave, self).__init__(*args, **kwargs) 35 | self.__echoDX = kwargs.get("echoDX", True) 36 | self.__echoDXSize = kwargs.get("echoDXSize", None) 37 | self.__pollQueue = [] 38 | 39 | def __msg(self, message): 40 | if self.debug: 41 | print("CpPhyDummySlave: %s" % message) 42 | 43 | def close(self): 44 | """Close the PHY device. 45 | """ 46 | self.__pollQueue = [] 47 | super(CpPhyDummySlave, self).close() 48 | 49 | def sendData(self, telegramData, srd): 50 | """Send data to the physical line. 51 | """ 52 | telegramData = bytearray(telegramData) 53 | self.__msg("Sending %s %s" % ("SRD" if srd else "SDN", 54 | bytesToHex(telegramData))) 55 | self.__mockSend(telegramData, srd = srd) 56 | 57 | def pollData(self, timeout=0.0): 58 | """Poll received data from the physical line. 59 | timeout => timeout in seconds. 60 | 0.0 = no timeout, return immediately. 61 | negative = unlimited. 62 | """ 63 | try: 64 | telegramData = self.__pollQueue.pop(0) 65 | except IndexError as e: 66 | return None 67 | self.__msg("Receiving %s" % bytesToHex(telegramData)) 68 | return telegramData 69 | 70 | def setConfig(self, baudrate=CpPhy.BAUD_9600, *args, **kwargs): 71 | self.__msg("Baudrate = %d" % baudrate) 72 | self.__pollQueue = [] 73 | super(CpPhyDummySlave, self).setConfig(baudrate=baudrate, *args, **kwargs) 74 | 75 | def __mockSend(self, telegramData, srd): 76 | if not srd: 77 | return 78 | try: 79 | fdl = FdlTelegram.fromRawData(telegramData) 80 | 81 | if (fdl.fc & FdlTelegram.FC_REQFUNC_MASK) == FdlTelegram.FC_FDL_STAT: 82 | telegram = FdlTelegram_FdlStat_Con(da = fdl.sa, 83 | sa = fdl.da) 84 | self.__pollQueue.append(telegram.getRawData()) 85 | return 86 | 87 | dp = DpTelegram.fromFdlTelegram(fdl, thisIsMaster = False) 88 | 89 | if DpTelegram_SlaveDiag_Req.checkType(dp): 90 | telegram = DpTelegram_SlaveDiag_Con(da = fdl.sa, 91 | sa = fdl.da) 92 | telegram.b1 |= DpTelegram_SlaveDiag_Con.B1_ONE 93 | self.__pollQueue.append(telegram.toFdlTelegram().getRawData()) 94 | return 95 | if DpTelegram_SetPrm_Req.checkType(dp): 96 | telegram = FdlTelegram_ack() 97 | self.__pollQueue.append(telegram.getRawData()) 98 | return 99 | if DpTelegram_ChkCfg_Req.checkType(dp): 100 | telegram = FdlTelegram_ack() 101 | self.__pollQueue.append(telegram.getRawData()) 102 | return 103 | if DpTelegram_DataExchange_Req.checkType(dp): 104 | if self.__echoDX: 105 | du = bytearray([ d ^ 0xFF for d in dp.du ]) 106 | if self.__echoDXSize is not None: 107 | if len(du) > self.__echoDXSize: 108 | du = du[ : self.__echoDXSize] 109 | if len(du) < self.__echoDXSize: 110 | du += bytearray(self.__echoDXSize - len(du)) 111 | telegram = DpTelegram_DataExchange_Con(da = fdl.sa, 112 | sa = fdl.da, 113 | du = du) 114 | self.__pollQueue.append(telegram.toFdlTelegram().getRawData()) 115 | else: 116 | telegram = FdlTelegram_ack() 117 | self.__pollQueue.append(telegram.getRawData()) 118 | return 119 | 120 | self.__msg("Dropping SRD telegram: %s" % str(fdl)) 121 | except ProfibusError as e: 122 | text = "SRD mock-send error: %s" % str(e) 123 | self.__msg(text) 124 | raise PhyError(text) 125 | -------------------------------------------------------------------------------- /pyprofibus/phy_fpga.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # PROFIBUS DP - Communication Processor PHY access library 4 | # 5 | # Copyright (c) 2019 Michael Buesch 6 | # 7 | # Licensed under the terms of the GNU General Public License version 2, 8 | # or (at your option) any later version. 9 | # 10 | 11 | from __future__ import division, absolute_import, print_function, unicode_literals 12 | from pyprofibus.compat import * 13 | 14 | from pyprofibus.phy import * 15 | from pyprofibus.fdl import * 16 | from pyprofibus.dp import * 17 | from pyprofibus.phy_fpga_driver import * 18 | 19 | from collections import deque 20 | 21 | 22 | __all__ = [ 23 | "CpPhyFPGA", 24 | ] 25 | 26 | 27 | class CpPhyFPGA(CpPhy): 28 | """FPGA based PROFIBUS CP PHYsical layer 29 | """ 30 | 31 | PFX = "PHY-fpga: " 32 | 33 | def __init__(self, spiBus, spiCS, spiSpeedHz, *args, **kwargs): 34 | super(CpPhyFPGA, self).__init__(*args, **kwargs) 35 | self.__rxDeque = deque() 36 | self.__driver = None 37 | self.__spiBus = spiBus 38 | self.__spiCS = spiCS 39 | self.__spiSpeedHz = spiSpeedHz 40 | 41 | def close(self): 42 | """Close the PHY device. 43 | """ 44 | if self.__driver is not None: 45 | try: 46 | self.__driver.shutdown() 47 | except FpgaPhyError as e: 48 | pass 49 | self.__rxDeque.clear() 50 | self.__driver = None 51 | super(CpPhyFPGA, self).close() 52 | 53 | def __tryRestartDriver(self, exception): 54 | try: 55 | self._debugMsg("Driver exception: %s" % str(exception)) 56 | if self.__driver is not None: 57 | self.__driver.restart() 58 | except FpgaPhyError as e: 59 | self._debugMsg("Error recovery restart failed: %s" % ( 60 | str(e))) 61 | 62 | def sendData(self, telegramData, srd): 63 | """Send data to the physical line. 64 | """ 65 | if self.__driver is None: 66 | return 67 | 68 | if self.debug: 69 | self._debugMsg("TX %s" % bytesToHex(telegramData)) 70 | 71 | try: 72 | self.__driver.telegramSend(telegramData) 73 | except FpgaPhyError as e: 74 | self.__tryRestartDriver(e) 75 | 76 | def pollData(self, timeout=0.0): 77 | """Poll received data from the physical line. 78 | timeout => timeout in seconds. 79 | 0 = no timeout, return immediately. 80 | negative = unlimited. 81 | """ 82 | if self.__driver is None: 83 | return None 84 | 85 | telegramData = None 86 | try: 87 | if self.__rxDeque: 88 | telegramData = self.__rxDeque.popleft() 89 | else: 90 | timeoutStamp = monotonic_time() + timeout#TODO 91 | telegramDataList = self.__driver.telegramReceive() 92 | count = len(telegramDataList) 93 | if count >= 1: 94 | telegramData = telegramDataList[0] 95 | if count >= 2: 96 | self.__rxDeque.extend(telegramDataList[1:]) 97 | except FpgaPhyError as e: 98 | self.__tryRestartDriver(e) 99 | telegramData = None 100 | 101 | if self.debug and telegramData: 102 | self._debugMsg("RX %s" % bytesToHex(telegramData)) 103 | return telegramData 104 | 105 | def setConfig(self, baudrate=CpPhy.BAUD_9600, *args, **kwargs): 106 | super(CpPhyFPGA, self).setConfig(baudrate=baudrate, *args, **kwargs) 107 | self.close() 108 | try: 109 | self.__driver = FpgaPhyDriver(spiDev=self.__spiBus, 110 | spiChipSelect=self.__spiCS, 111 | spiSpeedHz=self.__spiSpeedHz) 112 | self.__driver.setBaudRate(baudrate) 113 | except FpgaPhyError as e: 114 | raise PhyError(self.PFX + ("Failed to setup driver:\n%s" % str(e))) 115 | -------------------------------------------------------------------------------- /pyprofibus/phy_fpga_driver/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, absolute_import, print_function, unicode_literals 2 | from pyprofibus.compat import * 3 | 4 | from pyprofibus.phy_fpga_driver.exceptions import * 5 | from pyprofibus.phy_fpga_driver.driver import * 6 | -------------------------------------------------------------------------------- /pyprofibus/phy_fpga_driver/driver.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Driver for FPGA based PROFIBUS PHY. 4 | # 5 | # Copyright (c) 2019 Michael Buesch 6 | # 7 | # Licensed under the terms of the GNU General Public License version 2, 8 | # or (at your option) any later version. 9 | # 10 | 11 | from __future__ import division, absolute_import, print_function, unicode_literals 12 | from pyprofibus.compat import * 13 | 14 | import time 15 | 16 | from pyprofibus.phy_fpga_driver.exceptions import * 17 | from pyprofibus.phy_fpga_driver.messages import * 18 | from pyprofibus.phy_fpga_driver.io import * 19 | from pyprofibus.util import monotonic_time, FaultDebouncer 20 | 21 | 22 | __all__ = [ 23 | "FpgaPhyDriver", 24 | ] 25 | 26 | 27 | class FpgaPhyDriver(object): 28 | """Driver for FPGA based PROFIBUS PHY. 29 | """ 30 | 31 | FPGA_CLK_HZ = 24 * 1e6 32 | PING_INTERVAL = 0.1 33 | DEB_INTERVAL = 1.0 34 | 35 | def __init__(self, spiDev=0, spiChipSelect=0, spiSpeedHz=1000000): 36 | self.__baudrate = 9600 37 | self.__ioProc = None 38 | self.__nextPing = monotonic_time() 39 | self.__receivedPong = False 40 | self.__spiDev = spiDev 41 | self.__spiChipSelect = spiChipSelect 42 | self.__spiSpeedHz = spiSpeedHz 43 | 44 | try: 45 | self.__startup() 46 | except FpgaPhyError as e: 47 | try: 48 | self.shutdown() 49 | except FpgaPhyError: 50 | pass 51 | raise e 52 | 53 | def __startup(self): 54 | """Startup the driver. 55 | """ 56 | self.shutdown() 57 | 58 | self.__faultParity = FaultDebouncer() 59 | self.__faultMagic = FaultDebouncer() 60 | self.__faultLen = FaultDebouncer() 61 | self.__faultPBLen = FaultDebouncer() 62 | self.__nextFaultDebounce = monotonic_time() + self.DEB_INTERVAL 63 | 64 | # Start the communication process. 65 | self.__ioProc = FpgaPhyProc(self.__spiDev, self.__spiChipSelect, self.__spiSpeedHz) 66 | if not self.__ioProc.start(): 67 | self.__ioProc = None 68 | raise FpgaPhyError("Failed to start I/O process.") 69 | 70 | # Reset the FPGA. 71 | # But first ping the device to make sure SPI communication works. 72 | self.__ping() 73 | self.__controlSend(FpgaPhyMsgCtrl(FpgaPhyMsgCtrl.SPICTRL_SOFTRESET)) 74 | time.sleep(0.01) 75 | self.__ping() 76 | 77 | # Get the FPGA status to clear all errors. 78 | self.__fetchStatus() 79 | 80 | # Clear all event counters in I/O proc. 81 | self.__ioProc.getEventStatus() 82 | 83 | self.__nextPing = monotonic_time() + self.PING_INTERVAL 84 | self.__receivedPong = True 85 | 86 | def __ping(self, tries=3): 87 | """Ping the FPGA and check if a pong can be received. 88 | Raises a FpgaPhyError on failure. 89 | """ 90 | for i in range(tries - 1, -1, -1): 91 | try: 92 | pingMsg = FpgaPhyMsgCtrl(FpgaPhyMsgCtrl.SPICTRL_PING) 93 | pongMsg = self.__controlTransferSync(pingMsg, 94 | FpgaPhyMsgCtrl.SPICTRL_PONG) 95 | if not pongMsg: 96 | raise FpgaPhyError("Cannot communicate with " 97 | "PHY. Timeout.") 98 | break 99 | except FpgaPhyError as e: 100 | if i <= 0: 101 | raise e 102 | 103 | def __fetchStatus(self): 104 | """Fetch the FPGA status. 105 | """ 106 | txMsg = FpgaPhyMsgCtrl(FpgaPhyMsgCtrl.SPICTRL_GETSTATUS) 107 | rxMsg = self.__controlTransferSync(txMsg, FpgaPhyMsgCtrl.SPICTRL_STATUS) 108 | if not rxMsg: 109 | raise FpgaPhyError("Failed to get status.") 110 | return rxMsg.ctrlData 111 | 112 | def shutdown(self): 113 | """Shutdown the driver. 114 | """ 115 | if self.__ioProc is not None: 116 | self.__ioProc.shutdownProc() 117 | self.__ioProc = None 118 | 119 | def restart(self): 120 | """Restart the driver and the FPGA. 121 | """ 122 | self.__startup() 123 | self.setBaudRate(self.__baudrate) 124 | 125 | def setBaudRate(self, baudrate): 126 | """Configure the PHY baud rate. 127 | """ 128 | if self.__ioProc is None: 129 | raise FpgaPhyError("Cannot set baud rate. " 130 | "Driver not initialized.") 131 | if baudrate < 9600 or baudrate > 12000000: 132 | raise FpgaPhyError("Invalid baud rate %d." % baudrate) 133 | 134 | clksPerSym = int(round(self.FPGA_CLK_HZ / baudrate)) 135 | if not (1 <= clksPerSym <= 0xFFFFFF): 136 | raise FpgaPhyError("Invalid baud rate %d. " 137 | "CLK divider out of range." % baudrate) 138 | 139 | realBaudrate = int(round(self.FPGA_CLK_HZ / clksPerSym)) 140 | baudError = abs(baudrate - realBaudrate) / baudrate 141 | maxError = 0.005 142 | if baudError > maxError: 143 | raise FpgaPhyError("Invalid baud rate %d. " 144 | "CLK divider maximum error threshold (%.1f%%) exceed " 145 | "(actual error = %.1f%%)." % ( 146 | baudrate, 147 | maxError * 100.0, 148 | baudError * 100.0)) 149 | 150 | txMsg = FpgaPhyMsgCtrl(FpgaPhyMsgCtrl.SPICTRL_BAUD, 151 | ctrlData=clksPerSym) 152 | rxMsg = self.__controlTransferSync(txMsg, FpgaPhyMsgCtrl.SPICTRL_BAUD) 153 | if not rxMsg or rxMsg.ctrlData != txMsg.ctrlData: 154 | raise FpgaPhyError("Failed to set baud rate.") 155 | self.__baudrate = baudrate 156 | 157 | def __controlTransferSync(self, ctrlMsg, rxCtrlMsgId): 158 | """Transfer a control message and wait for a reply. 159 | """ 160 | self.__controlSend(ctrlMsg) 161 | for j in range(50): 162 | for rxMsg in self.__controlReceive(): 163 | if rxMsg.ctrl == rxCtrlMsgId: 164 | return rxMsg 165 | time.sleep(0.01) 166 | return None 167 | 168 | def __controlSend(self, ctrlMsg): 169 | """Send a FpgaPhyMsgCtrl() control message. 170 | """ 171 | self.__ioProc.controlSend(ctrlMsg) 172 | 173 | def __controlReceive(self): 174 | """Get a list of received control messages. 175 | Returns a list of FpgaPhyMsgCtrl(). 176 | The returned list might be empty. 177 | """ 178 | return self.__ioProc.controlReceive() 179 | 180 | def __handleControl(self): 181 | """Receive and handle pending control messages. 182 | """ 183 | rxMsgs = self.__controlReceive() 184 | for rxMsg in rxMsgs: 185 | ctrl = rxMsg.ctrl 186 | if ctrl == FpgaPhyMsgCtrl.SPICTRL_NOP: 187 | pass # Nothing to do. 188 | elif ctrl == FpgaPhyMsgCtrl.SPICTRL_PONG: 189 | self.__receivedPong = True 190 | else: 191 | raise FpgaPhyError("Received unexpected " 192 | "control message: %s" % str(rxMsg)) 193 | 194 | def __handleEvents(self, events): 195 | if events & (1 << FpgaPhyProc.EVENT_RESET): 196 | statusBits = self.__fetchStatus() 197 | info = [] 198 | if statusBits & (1 << FpgaPhyMsgCtrl.SPISTAT_PONRESET): 199 | info.append("power-on-reset") 200 | if statusBits & (1 << FpgaPhyMsgCtrl.SPISTAT_HARDRESET): 201 | info.append("hard-reset") 202 | if statusBits & (1 << FpgaPhyMsgCtrl.SPISTAT_SOFTRESET): 203 | info.append("soft-reset") 204 | info.append("0x%02X" % statusBits) 205 | raise FpgaPhyError("Reset detected (%s)." % ( 206 | " / ".join(info))) 207 | 208 | if events & (1 << FpgaPhyProc.EVENT_NEWSTAT): 209 | statusBits = self.__fetchStatus() 210 | if statusBits & (1 << FpgaPhyMsgCtrl.SPISTAT_TXOVR): 211 | raise FpgaPhyError("FPGA TX buffer overflow.") 212 | if statusBits & (1 << FpgaPhyMsgCtrl.SPISTAT_RXOVR): 213 | raise FpgaPhyError("FPGA RX buffer overflow.") 214 | if statusBits & (1 << FpgaPhyMsgCtrl.SPISTAT_CTRLCRCERR): 215 | raise FpgaPhyError("FPGA control message CRC error.") 216 | 217 | if events & (1 << FpgaPhyProc.EVENT_PARERR): 218 | self.__faultParity.fault() 219 | else: 220 | self.__faultParity.ok() 221 | 222 | if events & (1 << FpgaPhyProc.EVENT_NOMAGIC): 223 | self.__faultMagic.fault() 224 | else: 225 | self.__faultMagic.ok() 226 | 227 | if events & (1 << FpgaPhyProc.EVENT_INVALLEN): 228 | self.__faultLen.fault() 229 | else: 230 | self.__faultLen.ok() 231 | 232 | if events & (1 << FpgaPhyProc.EVENT_PBLENERR): 233 | self.__faultPBLen.fault() 234 | else: 235 | self.__faultPBLen.ok() 236 | 237 | if self.__faultParity.get() >= 3: 238 | raise FpgaPhyError("Detected FPGA message parity errors.") 239 | if self.__faultMagic.get() >= 3: 240 | raise FpgaPhyError("Detected FPGA message MAGC-field errors.") 241 | if self.__faultLen.get() >= 3: 242 | raise FpgaPhyError("Detected FPGA message LEN-field errors.") 243 | if self.__faultPBLen.get() >= 5: 244 | raise FpgaPhyError("Detected Profibus telegram LEN-field errors.") 245 | 246 | def telegramSend(self, txTelegramData): 247 | """Send a PROFIBUS telegram. 248 | """ 249 | ioProc = self.__ioProc 250 | if ioProc is None: 251 | raise FpgaPhyError("telegramSend: No I/O process") 252 | 253 | now = monotonic_time() 254 | 255 | # Handle keep-alive-ping. 256 | if now >= self.__nextPing: 257 | if not self.__receivedPong: 258 | # We did not receive the PONG to the previous PING. 259 | raise FpgaPhyError("PING to FPGA failed.") 260 | self.__nextPing = now + self.PING_INTERVAL 261 | self.__receivedPong = False 262 | # Send a PING to the FPGA to check if it is still alive. 263 | pingMsg = FpgaPhyMsgCtrl(FpgaPhyMsgCtrl.SPICTRL_PING) 264 | self.__controlSend(pingMsg) 265 | 266 | # Send the telegram data. 267 | ioProc.dataSend(txTelegramData) 268 | 269 | def telegramReceive(self): 270 | """Get a list of received PROFIBUS telegrams. 271 | Returns a list of bytes. 272 | The returned list might be empty. 273 | """ 274 | ioProc = self.__ioProc 275 | if ioProc is None: 276 | raise FpgaPhyError("telegramReceive: No I/O process") 277 | 278 | rxTelegrams = [] 279 | now = monotonic_time() 280 | 281 | # Handle I/O process events. 282 | events = ioProc.getEventStatus() 283 | if events: 284 | self.__nextFaultDebounce = now + self.DEB_INTERVAL 285 | self.__handleEvents(events) 286 | elif now >= self.__nextFaultDebounce: 287 | self.__nextFaultDebounce = now + self.DEB_INTERVAL 288 | self.__handleEvents(0) # No events 289 | 290 | # Handle received control data. 291 | if ioProc.controlAvailable(): 292 | self.__nextPing = now + self.PING_INTERVAL 293 | self.__handleControl() 294 | 295 | # Handle received telegram data. 296 | if ioProc.dataAvailable(): 297 | self.__nextPing = now + self.PING_INTERVAL 298 | rxTelegrams = ioProc.dataReceive() 299 | 300 | return rxTelegrams 301 | -------------------------------------------------------------------------------- /pyprofibus/phy_fpga_driver/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Driver for FPGA based PROFIBUS PHY. 4 | # 5 | # Copyright (c) 2019 Michael Buesch 6 | # 7 | # Licensed under the terms of the GNU General Public License version 2, 8 | # or (at your option) any later version. 9 | # 10 | 11 | from __future__ import division, absolute_import, print_function, unicode_literals 12 | from pyprofibus.compat import * 13 | 14 | from pyprofibus.phy import PhyError 15 | 16 | 17 | __all__ = [ 18 | "FpgaPhyError", 19 | ] 20 | 21 | 22 | class FpgaPhyError(PhyError): 23 | def __init__(self, msg, *args, **kwargs): 24 | msg = "PHY-FPGA: " + str(msg) 25 | super(FpgaPhyError, self).__init__(msg, *args, **kwargs) 26 | -------------------------------------------------------------------------------- /pyprofibus/phy_fpga_driver/messages.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Driver for FPGA based PROFIBUS PHY. 4 | # 5 | # Copyright (c) 2019 Michael Buesch 6 | # 7 | # Licensed under the terms of the GNU General Public License version 2, 8 | # or (at your option) any later version. 9 | # 10 | 11 | from __future__ import division, absolute_import, print_function, unicode_literals 12 | from pyprofibus.compat import * 13 | 14 | from pyprofibus.phy_fpga_driver.exceptions import * 15 | 16 | 17 | __all__ = [ 18 | "FpgaPhyMsg", 19 | "FpgaPhyMsgCtrl", 20 | ] 21 | 22 | 23 | class FpgaPhyMsg(object): 24 | SPI_MS_MAGIC = 0xAA 25 | SPI_SM_MAGIC = 0x55 26 | 27 | PADDING = 0x00 28 | PADDING_BYTE = bytes([PADDING]) 29 | 30 | SPI_FLG_START = 0 31 | SPI_FLG_CTRL = 1 32 | SPI_FLG_NEWSTAT = 2 33 | SPI_FLG_RESET = 3 34 | SPI_FLG_UNUSED4 = 4 35 | SPI_FLG_UNUSED5 = 5 36 | SPI_FLG_UNUSED7 = 6 37 | SPI_FLG_PARITY = 7 38 | 39 | LEN_UNKNOWN = -1 40 | LEN_NEEDMORE = -2 41 | LEN_ERROR = -3 42 | 43 | CRC_POLYNOMIAL = 0x07 44 | 45 | SD1 = 0x10 46 | SD2 = 0x68 47 | SD3 = 0xA2 48 | SD4 = 0xDC 49 | SC = 0xE5 50 | 51 | @staticmethod 52 | def crc8(dataBytes, crc=0xFF, P=CRC_POLYNOMIAL): 53 | for data in dataBytes: 54 | data ^= crc 55 | for i in range(8): 56 | data = ((data << 1) ^ (P if (data & 0x80) else 0)) & 0xFF 57 | crc = data 58 | return crc 59 | 60 | @classmethod 61 | def parity(cls, value): 62 | """Calculate odd parity on 8 bits. 63 | """ 64 | return ((value & 1) ^ 65 | ((value >> 1) & 1) ^ 66 | ((value >> 2) & 1) ^ 67 | ((value >> 3) & 1) ^ 68 | ((value >> 4) & 1) ^ 69 | ((value >> 5) & 1) ^ 70 | ((value >> 6) & 1) ^ 71 | ((value >> 7) & 1) ^ 1) 72 | 73 | @classmethod 74 | def calcLen(cls, dataBytes): 75 | dataBytesLen = len(dataBytes) 76 | if dataBytesLen > 0: 77 | firstByte = dataBytes[0] 78 | if firstByte == cls.SC: 79 | return 1 80 | elif firstByte == cls.SD1: 81 | return 6 82 | elif firstByte == cls.SD2: 83 | if dataBytesLen >= 4: 84 | lenField = dataBytes[1] 85 | if (lenField == dataBytes[2] and 86 | lenField >= 4 and 87 | lenField <= 249 and 88 | dataBytes[3] == cls.SD2): 89 | return lenField + 6 90 | else: 91 | return cls.LEN_ERROR 92 | return cls.LEN_NEEDMORE 93 | elif firstByte == cls.SD3: 94 | return 14 95 | elif firstByte == cls.SD4: 96 | return 3 97 | return cls.LEN_UNKNOWN 98 | 99 | class FpgaPhyMsgCtrl(FpgaPhyMsg): 100 | # SPI control message IDs. 101 | SPICTRL_NOP = 0 102 | SPICTRL_PING = 1 103 | SPICTRL_PONG = 2 104 | SPICTRL_SOFTRESET = 3 105 | SPICTRL_GETSTATUS = 4 106 | SPICTRL_STATUS = 5 107 | SPICTRL_GETBAUD = 6 108 | SPICTRL_BAUD = 7 109 | 110 | # Status message data bits. 111 | SPISTAT_PONRESET = 0 112 | SPISTAT_HARDRESET = 1 113 | SPISTAT_SOFTRESET = 2 114 | SPISTAT_TXOVR = 3 115 | SPISTAT_RXOVR = 4 116 | SPISTAT_CTRLCRCERR = 5 117 | 118 | CTRL_LEN = 8 119 | 120 | def __init__(self, ctrl, ctrlData=0, flg=0): 121 | self.flg = flg | (1 << self.SPI_FLG_CTRL) 122 | self.ctrl = ctrl 123 | self.ctrlData = ctrlData 124 | 125 | def toBytes(self): 126 | data = bytearray(8) 127 | data[0] = self.SPI_MS_MAGIC 128 | data[1] = 1 << self.SPI_FLG_CTRL 129 | data[1] |= self.parity(data[1]) << self.SPI_FLG_PARITY 130 | data[2] = self.ctrl & 0xFF 131 | data[3] = (self.ctrlData >> 24) & 0xFF 132 | data[4] = (self.ctrlData >> 16) & 0xFF 133 | data[5] = (self.ctrlData >> 8) & 0xFF 134 | data[6] = self.ctrlData & 0xFF 135 | data[7] = self.crc8(data[2:7]) 136 | return data 137 | 138 | @classmethod 139 | def fromBytes(cls, data): 140 | if data[0] != cls.SPI_SM_MAGIC: 141 | raise FpgaPhyError("FPGA control message: " 142 | "Invalid MAGC field.") 143 | flg = data[1] 144 | if cls.parity(flg): 145 | raise FpgaPhyError("FPGA control message: " 146 | "Invalid parity bit.") 147 | if not (flg & (1 << cls.SPI_FLG_CTRL)): 148 | raise FpgaPhyError("FPGA control message: " 149 | "CTRL bit is not set.") 150 | ctrl = data[2] 151 | ctrlData = data[3] << 24 152 | ctrlData |= data[4] << 16 153 | ctrlData |= data[5] << 8 154 | ctrlData |= data[6] 155 | crc = data[7] 156 | crcExpected = cls.crc8(data[2:7]) 157 | if crc != crcExpected: 158 | raise FpgaPhyError("FPGA control message: " 159 | "CRC error.") 160 | return cls(ctrl, ctrlData, flg) 161 | 162 | def __str__(self): 163 | try: 164 | name = { 165 | self.SPICTRL_NOP : "NOP", 166 | self.SPICTRL_PING : "PING", 167 | self.SPICTRL_PONG : "PONG", 168 | self.SPICTRL_SOFTRESET : "SOFTRESET", 169 | self.SPICTRL_GETSTATUS : "GETSTATUS", 170 | self.SPICTRL_STATUS : "STATUS", 171 | self.SPICTRL_GETBAUD : "GETBAUD", 172 | self.SPICTRL_BAUD : "BAUD", 173 | }[self.ctrl] 174 | except KeyError as e: 175 | name = "%02X" % self.ctrl 176 | return "PHYControl(%s, 0x%08X)" % (name, self.ctrlData) 177 | -------------------------------------------------------------------------------- /pyprofibus/phy_serial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # PROFIBUS DP - Communication Processor PHY access library 4 | # 5 | # Copyright (c) 2016-2021 Michael Buesch 6 | # 7 | # Licensed under the terms of the GNU General Public License version 2, 8 | # or (at your option) any later version. 9 | # 10 | 11 | from __future__ import division, absolute_import, print_function, unicode_literals 12 | from pyprofibus.compat import * 13 | 14 | from pyprofibus.phy import * 15 | from pyprofibus.fdl import FdlTelegram 16 | from pyprofibus.util import * 17 | 18 | import sys 19 | 20 | try: 21 | import serial 22 | except ImportError as e: 23 | if "PyPy" in sys.version and\ 24 | sys.version_info[0] == 2: 25 | # We are on PyPy2. 26 | # Try to import CPython2's serial. 27 | import glob 28 | sys.path.extend(glob.glob("/usr/lib/python2*/*-packages/")) 29 | import serial 30 | else: 31 | raise e 32 | try: 33 | import serial.rs485 34 | except ImportError: 35 | pass 36 | 37 | __all__ = [ 38 | "CpPhySerial", 39 | ] 40 | 41 | class CpPhySerial(CpPhy): 42 | """pyserial based PROFIBUS CP PHYsical layer 43 | """ 44 | 45 | __slots__ = ( 46 | "__discardTimeout", 47 | "__rxBuf", 48 | "__serial", 49 | ) 50 | 51 | PFX = "PHY-serial: " 52 | 53 | def __init__(self, port, useRS485Class=False, *args, **kwargs): 54 | """port => "/dev/ttySx" 55 | debug => enable/disable debugging. 56 | useRS485Class => Use serial.rs485.RS485, if True. (might be slower). 57 | """ 58 | super(CpPhySerial, self).__init__(*args, **kwargs) 59 | self.__discardTimeout = None 60 | self.__rxBuf = bytearray() 61 | try: 62 | if useRS485Class: 63 | if not hasattr(serial, "rs485"): 64 | raise PhyError("Module serial.rs485 " 65 | "is not available. " 66 | "Please use useRS485Class=False.") 67 | self.__serial = serial.rs485.RS485() 68 | else: 69 | self.__serial = serial.Serial() 70 | self.__serial.port = port 71 | self.__serial.baudrate = CpPhy.BAUD_9600 72 | self.__serial.bytesize = 8 73 | self.__serial.parity = serial.PARITY_EVEN 74 | self.__serial.stopbits = serial.STOPBITS_ONE 75 | self.__serial.timeout = 0 76 | self.__serial.xonxoff = False 77 | self.__serial.rtscts = False 78 | self.__serial.dsrdtr = False 79 | if useRS485Class: 80 | self.__serial.rs485_mode = serial.rs485.RS485Settings( 81 | rts_level_for_tx = True, 82 | rts_level_for_rx = False, 83 | loopback = False, 84 | delay_before_tx = 0.0, 85 | delay_before_rx = 0.0 86 | ) 87 | self.__serial.open() 88 | except (serial.SerialException, ValueError) as e: 89 | raise PhyError("Failed to open " 90 | "serial port:\n" + str(e)) 91 | 92 | def close(self): 93 | try: 94 | self.__serial.close() 95 | except serial.SerialException as e: 96 | pass 97 | self.__rxBuf = bytearray() 98 | super(CpPhySerial, self).close() 99 | 100 | def __discard(self): 101 | s = self.__serial 102 | if s: 103 | s.flushInput() 104 | s.flushOutput() 105 | if monotonic_time() >= self.__discardTimeout: 106 | self.__discardTimeout = None 107 | 108 | def __startDiscard(self): 109 | self.__discardTimeout = monotonic_time() + 0.01 110 | 111 | # Poll for received packet. 112 | # timeout => In seconds. 0.0 = none, Negative = unlimited. 113 | def pollData(self, timeout=0.0): 114 | if timeout > 0.0: 115 | timeoutStamp = monotonic_time() + timeout 116 | ret = None 117 | rxBuf = self.__rxBuf 118 | ser = self.__serial 119 | size = -1 120 | getSize = FdlTelegram.getSizeFromRaw 121 | 122 | while self.__discardTimeout is not None: 123 | self.__discard() 124 | if timeout > 0.0 and monotonic_time() >= timeoutStamp: 125 | return None 126 | 127 | try: 128 | rxBufLen = len(rxBuf) 129 | while True: 130 | if rxBufLen < 1: 131 | rxBuf.extend(ser.read(1)) 132 | elif rxBufLen < 3: 133 | if size < 0: 134 | size = getSize(rxBuf) 135 | readLen = (size if size > 0 else 3) - rxBufLen 136 | if readLen > 0: 137 | rxBuf.extend(ser.read(readLen)) 138 | elif rxBufLen >= 3: 139 | if size < 0: 140 | size = getSize(rxBuf) 141 | if size < 0: 142 | if self.debug and rxBuf: 143 | self._debugMsg("RX (fragment) %s" % bytesToHex(rxBuf)) 144 | rxBuf = bytearray() 145 | self.__startDiscard() 146 | raise PhyError("PHY-serial: " 147 | "Failed to get received telegram size: " 148 | "Invalid telegram format.") 149 | if rxBufLen < size: 150 | rxBuf.extend(ser.read(size - rxBufLen)) 151 | 152 | rxBufLen = len(rxBuf) 153 | if rxBufLen == size: 154 | ret = rxBuf 155 | rxBuf = bytearray() 156 | rxBufLen = 0 157 | break 158 | 159 | if (timeout == 0.0 or 160 | (timeout > 0.0 and monotonic_time() >= timeoutStamp)): 161 | break 162 | except serial.SerialException as e: 163 | if self.debug and rxBuf: 164 | self._debugMsg("RX (fragment) %s" % bytesToHex(rxBuf)) 165 | rxBuf = bytearray() 166 | self.__startDiscard() 167 | raise PhyError("PHY-serial: Failed to receive " 168 | "telegram:\n" + str(e)) 169 | finally: 170 | self.__rxBuf = rxBuf 171 | if self.debug and ret: 172 | self._debugMsg("RX %s" % bytesToHex(ret)) 173 | return ret 174 | 175 | def sendData(self, telegramData, srd): 176 | if self.__discardTimeout is not None: 177 | return 178 | try: 179 | if self.debug: 180 | self._debugMsg("TX %s" % bytesToHex(telegramData)) 181 | self.__serial.write(telegramData) 182 | except serial.SerialException as e: 183 | raise PhyError("PHY-serial: Failed to transmit " 184 | "telegram:\n" + str(e)) 185 | 186 | def setConfig(self, baudrate=CpPhy.BAUD_9600, rtscts=False, dsrdtr=False, *args, **kwargs): 187 | wellSuppBaud = (9600, 19200) 188 | if baudrate not in wellSuppBaud and not isMicropython: 189 | # The hw/driver might silently ignore the baudrate 190 | # and use the already set value from __init__(). 191 | self._warningMsg("The configured baud rate %d baud " 192 | "might not be supported by the hardware. " 193 | "Note that some hardware silently falls back " 194 | "to 9600 baud for unsupported rates. " 195 | "Commonly well supported baud rates by serial " 196 | "hardware are: %s." % ( 197 | baudrate, 198 | ", ".join(str(b) for b in wellSuppBaud))) 199 | try: 200 | if baudrate != self.__serial.baudrate or rtscts != self.__serial.rtscts or dsrdtr != self.__serial.dsrdtr: 201 | self.__serial.close() 202 | self.__serial.baudrate = baudrate 203 | self.__serial.rtscts = rtscts 204 | self.__serial.dsrdtr = dsrdtr 205 | self.__serial.open() 206 | self.__rxBuf = bytearray() 207 | except (serial.SerialException, ValueError) as e: 208 | raise PhyError("Failed to set CP-PHY " 209 | "configuration:\n" + str(e)) 210 | self.__setConfigPiLC(baudrate) 211 | super(CpPhySerial, self).setConfig(baudrate=baudrate, 212 | rtscts=rtscts, 213 | dsrdtr=dsrdtr, 214 | *args, **kwargs) 215 | 216 | def __setConfigPiLC(self, baudrate): 217 | """Reconfigure the PiLC HAT, if available. 218 | """ 219 | try: 220 | import libpilc.raspi_hat_conf as raspi_hat_conf 221 | except ImportError as e: 222 | return 223 | if not raspi_hat_conf.PilcConf.havePilcHat(): 224 | return 225 | try: 226 | conf = raspi_hat_conf.PilcConf() 227 | conf.setBaudrate(baudrate / 1000.0) 228 | except raspi_hat_conf.PilcConf.Error as e: 229 | raise PhyError("Failed to configure PiLC HAT:\n%s" %\ 230 | str(e)) 231 | -------------------------------------------------------------------------------- /pyprofibus/util.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Utility helpers 4 | # 5 | # Copyright (c) 2013-2020 Michael Buesch 6 | # 7 | # Licensed under the terms of the GNU General Public License version 2, 8 | # or (at your option) any later version. 9 | # 10 | 11 | from __future__ import division, absolute_import, print_function, unicode_literals 12 | from pyprofibus.compat import * 13 | 14 | import os 15 | import time 16 | import errno 17 | 18 | __all__ = [ 19 | "ProfibusError", 20 | "bytesToHex", 21 | "intToHex", 22 | "boolToStr", 23 | "fileExists", 24 | "monotonic_time", 25 | "TimeLimit", 26 | "FaultDebouncer", 27 | ] 28 | 29 | class ProfibusError(Exception): 30 | __slots__ = ( 31 | ) 32 | 33 | def bytesToHex(b, sep=" "): 34 | if b is None: 35 | return "None" 36 | assert isinstance(b, (bytes, bytearray)) 37 | if not b: 38 | return "Empty" 39 | return sep.join("%02X" % c for c in bytearray(b)) 40 | 41 | def intToHex(val): 42 | if val is None: 43 | return "None" 44 | assert isinstance(val, int) 45 | val &= 0xFFFFFFFF 46 | if val <= 0xFF: 47 | return "0x%02X" % val 48 | elif val <= 0xFFFF: 49 | return "0x%04X" % val 50 | elif val <= 0xFFFFFF: 51 | return "0x%06X" % val 52 | else: 53 | return "0x%08X" % val 54 | 55 | def boolToStr(val): 56 | return str(bool(val)) 57 | 58 | def fileExists(filename): 59 | """Returns True, if the file exists. 60 | Returns False, if the file does not exist. 61 | Returns None, if another error occurred. 62 | """ 63 | try: 64 | os.stat(filename) 65 | except OSError as e: 66 | if e.errno == errno.ENOENT: 67 | return False 68 | return None 69 | return True 70 | 71 | # Monotonic time. Returns a float second count. 72 | if isMicropython: 73 | def monotonic_time(): 74 | return time.ticks_ms() / 1e3 75 | else: 76 | monotonic_time = getattr(time, "monotonic", time.time) 77 | 78 | class TimeLimit(object): 79 | """Generic timeout helper. 80 | """ 81 | 82 | UNLIMITED = -1 # No limit 83 | 84 | __slots__ = ( 85 | "__limit", 86 | "__startTime", 87 | "__endTime", 88 | ) 89 | 90 | # limit => The time limit, in seconds. 91 | # Negative value = unlimited. 92 | def __init__(self, limit = 0): 93 | self.__limit = limit 94 | self.start() 95 | 96 | # (Re-)start the time. 97 | def start(self, limit = None): 98 | if limit is None: 99 | limit = self.__limit 100 | self.__limit = limit 101 | if limit >= 0: 102 | self.__startTime = monotonic_time() 103 | self.__endTime = self.__startTime + limit 104 | else: 105 | self.__startTime = self.__endTime = -1 106 | 107 | # Add seconds to the limit 108 | def add(self, seconds): 109 | if self.__limit >= 0: 110 | self.__limit += seconds 111 | self.__endTime = self.__startTime + self.__limit 112 | 113 | # Returns True, if the time limit exceed. 114 | def exceed(self): 115 | if self.__limit < 0: 116 | return False # Unlimited 117 | return monotonic_time() >= self.__endTime 118 | 119 | class FaultDebouncer(object): 120 | """Fault counter/debouncer. 121 | """ 122 | 123 | __slots__ = ( 124 | "__countMax", 125 | "__count", 126 | ) 127 | 128 | def __init__(self, countMax = 0xFFFF): 129 | self.__countMax = countMax 130 | self.reset() 131 | 132 | def reset(self): 133 | self.__count = 0 134 | 135 | def inc(self): 136 | if self.__count < self.__countMax - 2: 137 | self.__count += 2 138 | return (self.__count + 1) // 2 139 | 140 | def dec(self): 141 | if self.__count > 0: 142 | self.__count -= 1 143 | return (self.__count + 1) // 2 144 | 145 | fault = inc 146 | ok = dec 147 | 148 | def get(self): 149 | return (self.__count + 1) // 2 150 | -------------------------------------------------------------------------------- /pyprofibus/version.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, absolute_import, print_function, unicode_literals 2 | 3 | __all__ = [ 4 | "VERSION_MAJOR", 5 | "VERSION_MINOR", 6 | "VERSION_EXTRA", 7 | "VERSION_STRING", 8 | ] 9 | 10 | VERSION_MAJOR = 1 11 | VERSION_MINOR = 13 12 | VERSION_EXTRA = "" 13 | 14 | VERSION_STRING = "%d.%d%s" % (VERSION_MAJOR, VERSION_MINOR, VERSION_EXTRA) 15 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import print_function 5 | 6 | import sys, os 7 | basedir = os.path.abspath(os.path.dirname(__file__)) 8 | 9 | for base in (os.getcwd(), basedir): 10 | sys.path.insert(0, os.path.join(base, "misc")) 11 | # Add the basedir to PYTHONPATH before we try to import pyprofibus.version 12 | sys.path.insert(0, base) 13 | 14 | from setuptools import setup 15 | import setup_cython 16 | import warnings 17 | import re 18 | from pyprofibus.version import VERSION_STRING 19 | 20 | 21 | isWindows = os.name.lower() in {"nt", "ce"} 22 | isPosix = os.name.lower() == "posix" 23 | 24 | 25 | def getEnvInt(name, default = 0): 26 | try: 27 | return int(os.getenv(name, "%d" % default)) 28 | except ValueError: 29 | return default 30 | 31 | def getEnvBool(name, default = False): 32 | return bool(getEnvInt(name, 1 if default else 0)) 33 | 34 | buildCython = getEnvInt("PYPROFIBUS_CYTHON_BUILD", 3 if isPosix else 0) 35 | buildCython = ((buildCython == 1) or (buildCython == sys.version_info[0])) 36 | setup_cython.parallelBuild = bool(getEnvInt("PYPROFIBUS_CYTHON_PARALLEL", 1) == 1 or\ 37 | getEnvInt("PYPROFIBUS_CYTHON_PARALLEL", 1) == sys.version_info[0]) 38 | setup_cython.profileEnabled = bool(getEnvInt("PYPROFIBUS_PROFILE") > 0) 39 | setup_cython.debugEnabled = bool(getEnvInt("PYPROFIBUS_DEBUG_BUILD") > 0) 40 | 41 | def pyCythonPatchLine(line): 42 | # Patch the import statements 43 | line = re.sub(r'^(\s*from pyprofibus[0-9a-zA-Z_]*)\.([0-9a-zA-Z_\.]+) import', r'\1_cython.\2 import', line) 44 | line = re.sub(r'^(\s*from pyprofibus[0-9a-zA-Z_]*)\.([0-9a-zA-Z_\.]+) cimport', r'\1_cython.\2 cimport', line) 45 | line = re.sub(r'^(\s*import pyprofibus[0-9a-zA-Z_]*)\.', r'\1_cython.', line) 46 | line = re.sub(r'^(\s*cimport pyprofibus[0-9a-zA-Z_]*)\.', r'\1_cython.', line) 47 | return line 48 | 49 | setup_cython.pyCythonPatchLine = pyCythonPatchLine 50 | 51 | cmdclass = {} 52 | 53 | # Try to build the Cython modules. This might fail. 54 | if buildCython: 55 | buildCython = setup_cython.cythonBuildPossible() 56 | if buildCython: 57 | cmdclass["build_ext"] = setup_cython.CythonBuildExtension 58 | setup_cython.registerCythonModules() 59 | else: 60 | print("Skipping build of CYTHON modules.") 61 | 62 | ext_modules = setup_cython.ext_modules 63 | 64 | 65 | warnings.filterwarnings("ignore", r".*'long_description_content_type'.*") 66 | 67 | with open(os.path.join(basedir, "README.rst"), "rb") as fd: 68 | readmeText = fd.read().decode("UTF-8") 69 | 70 | setup( name = "pyprofibus", 71 | version = VERSION_STRING, 72 | description = "Python PROFIBUS module", 73 | license = "GNU General Public License v2 or later", 74 | author = "Michael Büsch", 75 | author_email = "m@bues.ch", 76 | url = "https://bues.ch/a/profibus", 77 | scripts = [ "gsdparser", 78 | "profisniff", 79 | "pyprofibus-linuxcnc-hal", ], 80 | packages = [ "pyprofibus", "pyprofibus.gsd", "pyprofibus.phy_fpga_driver" ], 81 | cmdclass = cmdclass, 82 | ext_modules = ext_modules, 83 | keywords = [ "PROFIBUS", "PROFIBUS-DP", "SPS", "PLC", 84 | "Step 7", "Siemens", 85 | "GSD", "GSD parser", "General Station Description", ], 86 | classifiers = [ 87 | "Development Status :: 4 - Beta", 88 | "Environment :: Console", 89 | "Intended Audience :: Developers", 90 | "Intended Audience :: Education", 91 | "Intended Audience :: Information Technology", 92 | "Intended Audience :: Manufacturing", 93 | "Intended Audience :: Science/Research", 94 | "License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)", 95 | "Operating System :: POSIX", 96 | "Operating System :: POSIX :: Linux", 97 | "Programming Language :: Cython", 98 | "Programming Language :: Python", 99 | "Programming Language :: Python :: 3", 100 | "Programming Language :: Python :: Implementation :: CPython", 101 | "Programming Language :: Python :: Implementation :: PyPy", 102 | "Programming Language :: Python :: Implementation :: MicroPython", 103 | "Topic :: Education", 104 | "Topic :: Home Automation", 105 | "Topic :: Scientific/Engineering", 106 | "Topic :: Scientific/Engineering :: Interface Engine/Protocol Translator", 107 | "Topic :: Scientific/Engineering :: Human Machine Interfaces", 108 | "Topic :: Software Development :: Embedded Systems", 109 | "Topic :: Software Development :: Libraries", 110 | "Topic :: System :: Hardware", 111 | "Topic :: System :: Hardware :: Hardware Drivers", 112 | "Topic :: System :: Networking", 113 | ], 114 | long_description=readmeText, 115 | long_description_content_type="text/x-rst", 116 | ) 117 | -------------------------------------------------------------------------------- /stublibs/README.rst: -------------------------------------------------------------------------------- 1 | pyprofibus stub libs 2 | ==================== 3 | 4 | This directory contains stubs, wrappers and minimal implementations of libraries used by pyprofibus. 5 | 6 | The purpose of these libraries is to support pyprofibus execution on restricted Python interpreters such as Micropython. 7 | -------------------------------------------------------------------------------- /stublibs/__future__.py: -------------------------------------------------------------------------------- 1 | division = absolute_import = print_function = unicode_literals = None 2 | -------------------------------------------------------------------------------- /stublibs/collections.py: -------------------------------------------------------------------------------- 1 | class deque(list): 2 | def popleft(self): 3 | return self.pop(0) 4 | -------------------------------------------------------------------------------- /stublibs/configparser.py: -------------------------------------------------------------------------------- 1 | class Error(Exception): 2 | pass 3 | 4 | class ConfigParser(object): 5 | class Section(object): 6 | def __init__(self, name): 7 | self.name = name 8 | self.options = {} 9 | 10 | def __init__(self): 11 | self.__sections = {} 12 | 13 | def read_file(self, f, source=None): 14 | self.__sections = {} 15 | section = None 16 | while True: 17 | line = f.readline() 18 | if not line: 19 | break 20 | line = line.lstrip().rstrip("\r\n") 21 | if not line or line.startswith(";"): 22 | continue 23 | sline = line.strip() 24 | if sline.startswith("[") and sline.endswith("]"): 25 | sectionName = sline[1:-1] 26 | if sectionName in self.__sections: 27 | raise Error("Multiple definitions of section '%s'" % sectionName) 28 | section = self.__sections[sectionName] = self.Section(sectionName) 29 | continue 30 | if section is None: 31 | raise Error("Option '%s' is not in a section." % line) 32 | idx = line.find("=") 33 | if idx > 0: 34 | optionName = line[:idx] 35 | optionValue = line[idx+1:] 36 | if optionName in section.options: 37 | raise Error("Multiple definitions of option '%s/%s'" % ( 38 | section.name, optionName)) 39 | section.options[optionName] = optionValue 40 | continue 41 | raise Error("Could not parse line: %s" % line) 42 | 43 | def has_option(self, section, option): 44 | try: 45 | self.get(section, option) 46 | except Error as e: 47 | return False 48 | return True 49 | 50 | def get(self, section, option): 51 | try: 52 | return self.__sections[section].options[option] 53 | except KeyError as e: 54 | raise Error("Option '%s/%s' not found." % (section, option)) 55 | 56 | def getboolean(self, section, option): 57 | try: 58 | v = self.get(section, option).lower().strip() 59 | if v == "true": 60 | return True 61 | if v == "false": 62 | return False 63 | return bool(int(v)) 64 | except ValueError as e: 65 | raise Error("Invalid boolean option '%s/%s'." % (section, option)) 66 | 67 | def getint(self, section, option): 68 | try: 69 | return int(self.get(section, option)) 70 | except ValueError as e: 71 | raise Error("Invalid int option '%s/%s'." % (section, option)) 72 | 73 | def sections(self): 74 | return list(self.__sections.keys()) 75 | 76 | def options(self, section): 77 | try: 78 | return list(self.__sections[section].options) 79 | except KeyError as e: 80 | raise Error("Section '%s' not found." % section) 81 | -------------------------------------------------------------------------------- /stublibs/difflib.py: -------------------------------------------------------------------------------- 1 | def get_close_matches(word, possibilities, n=3, cutoff=0.6): 2 | def fold(s): 3 | return s.lower().replace(" ", "").replace("\t", "") 4 | for p in possibilities: 5 | if fold(p) == fold(word): 6 | return [p] 7 | return [] 8 | -------------------------------------------------------------------------------- /stublibs/serial.py: -------------------------------------------------------------------------------- 1 | from pyprofibus.compat import * 2 | import time 3 | 4 | PARITY_EVEN = "E" 5 | PARITY_ODD = "O" 6 | STOPBITS_ONE = 1 7 | STOPBITS_TWO = 2 8 | 9 | class SerialException(Exception): 10 | pass 11 | 12 | class Serial(object): 13 | def __init__(self): 14 | self.__isMicropython = isMicropython 15 | self.port = "/dev/ttyS0" 16 | self.__portNum = None 17 | self.baudrate = 9600 18 | self.bytesize = 8 19 | self.parity = PARITY_EVEN 20 | self.stopbits = STOPBITS_ONE 21 | self.timeout = 0 22 | self.xonxoff = False 23 | self.rtscts = False 24 | self.dsrdtr = False 25 | self.__lowlevel = None 26 | 27 | def open(self): 28 | if self.__isMicropython: 29 | port = self.port 30 | for sub in ("/dev/ttyS", "/dev/ttyUSB", "/dev/ttyACM", "COM", "UART", ): 31 | port = port.replace(sub, "") 32 | try: 33 | self.__portNum = int(port.strip()) 34 | except ValueError: 35 | raise SerialException("Invalid port: %s" % self.port) 36 | try: 37 | import machine 38 | self.__lowlevel = machine.UART( 39 | self.__portNum, 40 | self.baudrate, 41 | self.bytesize, 42 | 0 if self.parity == PARITY_EVEN else 1, 43 | 1 if self.stopbits == STOPBITS_ONE else 2) 44 | print("Opened machine.UART(%d)" % self.__portNum) 45 | except Exception as e: 46 | raise SerialException("UART%d: Failed to open:\n%s" % ( 47 | self.__portNum, str(e))) 48 | return 49 | raise NotImplementedError 50 | 51 | def close(self): 52 | if self.__isMicropython: 53 | try: 54 | if self.__lowlevel is not None: 55 | self.__lowlevel.deinit() 56 | self.__lowlevel = None 57 | print("Closed machine.UART(%d)" % self.__portNum) 58 | except Exception as e: 59 | raise SerialException("UART%d: Failed to close:\n%s" % ( 60 | self.__portNum, str(e))) 61 | return 62 | raise NotImplementedError 63 | 64 | def write(self, data): 65 | if self.__isMicropython: 66 | try: 67 | self.__lowlevel.write(data) 68 | except Exception as e: 69 | raise SerialException("UART%d write(%d bytes) failed: %s" % ( 70 | self.__portNum, len(data), str(e))) 71 | return 72 | raise NotImplementedError 73 | 74 | def read(self, size=1): 75 | if self.__isMicropython: 76 | try: 77 | data = self.__lowlevel.read(size) 78 | if data is None: 79 | return b"" 80 | return data 81 | except Exception as e: 82 | raise SerialException("UART%d read(%d bytes) failed: %s" % ( 83 | self.__portNum, size, str(e))) 84 | raise NotImplementedError 85 | 86 | def flushInput(self): 87 | if self.__isMicropython: 88 | while self.__lowlevel.any(): 89 | self.__lowlevel.read() 90 | return 91 | raise NotImplementedError 92 | 93 | def flushOutput(self): 94 | if self.__isMicropython: 95 | time.sleep(0.01) 96 | return 97 | raise NotImplementedError 98 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from test_dummy import * 2 | from test_gsd import * 3 | -------------------------------------------------------------------------------- /tests/pyprofibus_tstlib.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, absolute_import, print_function, unicode_literals 2 | 3 | from unittest import TestCase 4 | 5 | __all__ = [ 6 | "TestCase", 7 | "initTest", 8 | ] 9 | 10 | 11 | def initTest(testCaseFile): 12 | from os.path import basename 13 | print("(test case file: %s)" % basename(testCaseFile)) 14 | -------------------------------------------------------------------------------- /tests/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # basedir is the root of the test directory in the package 4 | basedir="$(dirname "$0")" 5 | [ "$(echo "$basedir" | cut -c1)" = '/' ] || basedir="$PWD/$basedir" 6 | 7 | # rootdir is the root of the package 8 | rootdir="$basedir/.." 9 | 10 | 11 | die() 12 | { 13 | [ -n "$*" ] && echo "$*" >&2 14 | exit 1 15 | } 16 | 17 | # $1=interpreter 18 | # $2=test_dir 19 | run_pyunit() 20 | { 21 | local interpreter="$1" 22 | local test_dir="$2" 23 | 24 | ( 25 | echo 26 | echo "===" 27 | echo "= Running $interpreter..." 28 | echo "===" 29 | export PYTHONPATH="$rootdir/tests:$PYTHONPATH" 30 | cd "$rootdir" || die "Failed to cd to rootdir." 31 | "$interpreter" -m unittest --failfast --buffer --catch "$test_dir" ||\ 32 | die "Test failed" 33 | ) || die 34 | } 35 | 36 | test_interpreter() 37 | { 38 | "$1" -c 'import serial' >/dev/null 2>&1 39 | } 40 | 41 | # $1=test_dir 42 | run_testdir() 43 | { 44 | local test_dir="$1" 45 | 46 | unset PYTHONPATH 47 | unset PYTHONSTARTUP 48 | unset PYTHONY2K 49 | unset PYTHONOPTIMIZE 50 | unset PYTHONDEBUG 51 | export PYTHONDONTWRITEBYTECODE=1 52 | unset PYTHONINSPECT 53 | unset PYTHONIOENCODING 54 | unset PYTHONNOUSERSITE 55 | unset PYTHONUNBUFFERED 56 | unset PYTHONVERBOSE 57 | export PYTHONWARNINGS=once 58 | export PYTHONHASHSEED=random 59 | 60 | local exec_any=0 61 | if test_interpreter python2; then 62 | run_pyunit python2 "$test_dir" 63 | local exec_any=1 64 | fi 65 | if test_interpreter python3; then 66 | run_pyunit python3 "$test_dir" 67 | local exec_any=1 68 | fi 69 | if test_interpreter pypy3; then 70 | run_pyunit pypy3 "$test_dir" 71 | local exec_any=1 72 | fi 73 | if [ $exec_any -eq 0 ]; then 74 | die "Failed to find any usable Python interpreter." 75 | fi 76 | } 77 | 78 | run_tests() 79 | { 80 | run_testdir tests 81 | } 82 | 83 | run_tests 84 | -------------------------------------------------------------------------------- /tests/test_dummy.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, absolute_import, print_function, unicode_literals 2 | from pyprofibus_tstlib import * 3 | initTest(__file__) 4 | 5 | import pyprofibus 6 | import pyprofibus.conf 7 | import pyprofibus.dp 8 | import pyprofibus.dp_master 9 | import pyprofibus.phy_dummy 10 | import pyprofibus.phy_serial 11 | 12 | 13 | class Test_DummyPhy(TestCase): 14 | def test_dummy_phy(self): 15 | phy = pyprofibus.phy_dummy.CpPhyDummySlave(debug=True, echoDX=True) 16 | phy.setConfig(baudrate=19200) 17 | 18 | master = pyprofibus.DPM1(phy=phy, 19 | masterAddr=42, 20 | debug=True) 21 | 22 | conf = pyprofibus.conf.PbConf._SlaveConf() 23 | conf.addr = 84 24 | conf.inputSize = 1 25 | conf.outputSize = 1 26 | conf.diagPeriod = 0 27 | slaveDesc = pyprofibus.dp_master.DpSlaveDesc(conf) 28 | 29 | slaveDesc.setCfgDataElements([ 30 | pyprofibus.dp.DpCfgDataElement(pyprofibus.dp.DpCfgDataElement.ID_TYPE_OUT), 31 | pyprofibus.dp.DpCfgDataElement(pyprofibus.dp.DpCfgDataElement.ID_TYPE_IN), 32 | ]) 33 | 34 | slaveDesc.setUserPrmData(bytearray([1, 2, 3, 4, ])) 35 | 36 | slaveDesc.setSyncMode(True) 37 | slaveDesc.setFreezeMode(True) 38 | slaveDesc.setGroupMask(1) 39 | slaveDesc.setWatchdog(300) 40 | 41 | master.addSlave(slaveDesc) 42 | master.initialize() 43 | self.assertFalse(slaveDesc.isConnecting()) 44 | self.assertFalse(slaveDesc.isConnected()) 45 | 46 | # Run slave initialization state machine. 47 | for i in range(25): 48 | slaveDesc.setMasterOutData(bytearray([1, ])) 49 | master.run() 50 | if i == 1: 51 | self.assertTrue(slaveDesc.isConnecting()) 52 | self.assertFalse(slaveDesc.isConnected()) 53 | # Check dummy-slave response to Data_Exchange. 54 | for i in range(100): 55 | print("testing %d" % i) 56 | self.assertFalse(slaveDesc.isConnecting()) 57 | self.assertTrue(slaveDesc.isConnected()) 58 | j = 0 59 | while True: 60 | j += 1 61 | self.assertTrue(j < 10) 62 | slaveDesc.setMasterOutData(bytearray([i, ])) 63 | master.run() 64 | ret = slaveDesc.getMasterInData() 65 | if j >= 5 and ret is not None: 66 | break 67 | self.assertEqual(bytearray(ret), bytearray([i ^ 0xFF, ])) 68 | -------------------------------------------------------------------------------- /tests/test_gsd.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, absolute_import, print_function, unicode_literals 2 | from pyprofibus_tstlib import * 3 | initTest(__file__) 4 | 5 | import pyprofibus 6 | import pyprofibus.gsd 7 | import os 8 | 9 | 10 | class Test_GSD(TestCase): 11 | def test_modular(self): 12 | gsd = pyprofibus.gsd.GsdInterp.fromFile(os.path.join("misc", "dummy_modular.gsd")) 13 | gsd.setConfiguredModule("dummy input module") 14 | gsd.setConfiguredModule("dummy output module") 15 | 16 | self.assertEqual([ e.getDU() 17 | for e in gsd.getCfgDataElements() ], 18 | [ bytearray([0x00, ]), 19 | bytearray([0x10, ]), 20 | bytearray([0x20, ]), ]) 21 | self.assertEqual(gsd.getIdentNumber(), 0x4224) 22 | self.assertEqual(gsd.getUserPrmData(), bytearray([0x00, 0x00, 0x00, 0x42])) 23 | 24 | def test_compact(self): 25 | gsd = pyprofibus.gsd.GsdInterp.fromFile(os.path.join("misc", "dummy_compact.gsd")) 26 | self.assertEqual([ e.getDU() 27 | for e in gsd.getCfgDataElements() ], 28 | [ bytearray([0x00, ]), 29 | bytearray([0x10, ]), 30 | bytearray([0x20, ]), ]) 31 | self.assertEqual(gsd.getIdentNumber(), 0x4224) 32 | self.assertEqual(gsd.getUserPrmData(), bytearray([0x00, 0x00, 0x00, 0x42])) 33 | --------------------------------------------------------------------------------