This section will guide you through the process of adding new blocks to the OpenPLC Editor library.
DISCLAIMER: There is currently no “easy way” to add your custom block to the library without modifying the source code of the editor. All blocks are hard-coded in the source code of the editor, which means that if you want new blocks, you will have to write some code.
DISCLAIMER_2: When you update OpenPLC Editor, the entire source code at the installation folder is replaced with a more recent copy from the official repository on GitHub. This means that you will lose all changes you made to the original code. If you’re adding new blocks to the editor, please backup your changes before running an update.
Using OpenPLC Import Tool
OpenPLC Import Tool is a command-line utility written in Python that automatically makes all the necessary modifications on your OpenPLC Editor installation to add a library with custom blocks extracted from an OpenPLC project. The tool takes as input an .st file containing an OpenPLC program, and scans it looking for Functions and Function Blocks inside.
With the OpenPLC Import Tool users can write custom Functions and Function Blocks on OpenPLC Editor as usual on their own projects, and then use the tool to inject those custom Function and Function Blocks into OpenPLC Editor library, so that the blocks can be used on any project. Here are the steps required to insert new blocks using the OpenPLC Import Tool:
- Create an OpenPLC Project with the desired Functions and Function Blocks in it
- Generate an OpenPLC Runtime program (.st file) from your project. You can generate an .st file for your project by clicking on Generate program for OpenPLC Runtime (orange down arrow) on the tool bar and save the .st file on your computer.
- Use the OpenPLC Import Tool to import the function blocks found in the .st file
The only limitation about this method is that the block code is limited to IEC 61131-3 languages. Although it is possible to use { } pragmas to insert C code along with Structured Text code, this may not be the cleanest approach depending on what you want to accomplish. One alternative is to manually inspect the C files generated by the tool and then add whatever C code you want in them afterwards. If you want to go that route, it may be wise to understand the manual process first (especially the section “Writing C code for your blocks”) to get a better view of which files will need to be modified.
You can download OpenPLC Import Tool here. Usage:
python3 import_tool.py <filename.st>
Manual Approach
Instead of using the OpenPLC Import Tool, you can perform all the steps manually to add your own custom blocks to the OpenPLC Editor library. In summary, the steps to add a new block are:
- Modify the definitions.py file to add your custom library file to it
- Create a new .xml file for your custom blocks
- Modify MatIEC lib so that your blocks can be successfully compiled
- Write C code for your custom blocks
Modifying the definitions.py file
Go to the directory where OpenPLC Editor is installed in your machine. That’s usually C:\Users\[Your-User-Name]\OpenPLC_Editor\editor if you’re on Windows, or /Applications/OpenPLC Editor/Contents/Resources/editor if you’re on macOS. On Linux it will be the folder where you extracted the .zip installation. Open the definitions.py file on a text editor. This file is located inside the plcopen folder. Around line #50, add your custom library file:
StdTC6Libs = [(_(“Standard function blocks”), join(sd, “Standard_Function_Blocks.xml”)),
(_(“Additional function blocks”), join(sd, “Additional_Function_Blocks.xml”)),
(_(“My Custom Library”), join(sd, “My_Custom_Library.xml”))]
Creating a .xml file for your custom blocks
On the same plcopen folder, create a new .xml file for your library. On this example, we are calling it “My_Custom_Library.xml” (refer to the definitions.py entry above). This file will hold specific information about your library and the blocks it contains. It is written in the same format as the OpenPLC Editor projects (plcopen xml format). This means that if you don’t want to write it by hand, you can just create a new OpenPLC Editor project with your blocks as custom FunctionBlocks (click on the gray plus icon and select FunctionBlock) and then copy most of the content from your project’s plc.xml file over to your library .xml file. In the end, this is how your “My_Custom_Library.xml” should look like:
<?xml version=’1.0′ encoding=’utf-8′?>
<project xmlns:ns1=”http://www.plcopen.org/xml/tc6_0201″ xmlns:xhtml=”http://www.w3.org/1999/xhtml” xmlns:xsd=”http://www.w3.org/2001/XMLSchema” xmlns=”http://www.plcopen.org/xml/tc6_0201″>
<fileHeader companyName=”OpenPLC” productName=”My Custom Library” productVersion=”1.0″ creationDateTime=”2020-01-21T09:44:11″/>
<contentHeader name=”My Custom Library” author=”Thiago Alves” modificationDateTime=”2020-01-21T09:44:11″>
<coordinateInfo>
<fbd>
<scaling x=”0″ y=”0″/>
</fbd>
<ld>
<scaling x=”0″ y=”0″/>
</ld>
<sfc>
<scaling x=”0″ y=”0″/>
</sfc>
</coordinateInfo>
</contentHeader>
<types>
<dataTypes/>
<pous>
<pou name=”Test” pouType=”functionBlock”>
<interface>
<inputVars>
<variable name=”I0″>
<type>
<INT/>
</type>
<documentation>
<xhtml:p><![CDATA[Insert an integer here]]></xhtml:p>
</documentation>
</variable>
<variable name=”ACTIVE”>
<type>
<BOOL/>
</type>
<documentation>
<xhtml:p><![CDATA[0 – block does nothing, 1 – block sums 10 to integer in I0]]></xhtml:p>
</documentation>
</variable>
</inputVars>
<outputVars>
<variable name=”OUT”>
<type>
<INT/>
</type>
<documentation>
<xhtml:p><![CDATA[This is the output: I0 + 10]]></xhtml:p>
</documentation>
</variable>
</outputVars>
</interface>
<body>
<ST>
<xhtml:p><![CDATA[IF ACTIVE = TRUE THEN
OUT := I0 + 10;
END_IF;]]></xhtml:p>
</ST>
</body>
<documentation>
<xhtml:p><![CDATA[This block outputs the sum of input I0 and 10.]]></xhtml:p>
</documentation>
</pou>
</pous>
</types>
<instances>
<configurations/>
</instances>
</project>
Since this is a library file, you can add multiple function blocks to the same file. On this example we are adding just one: TEST. This block has two inputs – ACTIVE and I0, and one output – OUT. The block should output the sum of input I0 and 10 when ACTIVE is TRUE.
Save everything and your new block will show up in the editor now. This is enough to have the block there, but your code won’t compile just yet. You will have to add information about how to compile your new block in MatIEC. This is a bit tricky, so make sure you’re following all directions properly.
Modifying MatIEC lib
MatIEC is the transpiler that converts your PLC code to C code. It is an essential part of OpenPLC that allows it to be compatible with so many platforms – as C code is quite universal. There are two copies of MatIEC on your OpenPLC Editor installation – one for the simulator and another for the Arduino-compatible boards. If you want your blocks to compile on Arduino-compatible boards, you will have to modify both MatIEC copies. If your block contains Arduino-specific functionality, the simulator copy should contain only dummy code for your block (i.e. code that does nothing) so that the project can be compiled and simulated. On the Arduino copy though, you will have to write your Arduino-specific code for your block, as we will see below. If you’re writing generic code that can run anywhere (i.e. code that does not make use of Arduino-specific libraries or hardware), then you can have the same code on both MatIEC copies.
See below the location for each MatIEC copy inside your OpenPLC Editor installation:
- Simulator copy: /OpenPLC_Editor/matiec/lib
- Arduino-compatible copy /OpenPLC_Editor/editor/arduino/src/lib
Go to the MatIEC lib folder and create a file “my_custom_library.txt” with the full structured text code for all your custom blocks in your library. If you’re writing pure C code, this Structured Text code can be dummy – essentially blank. Following the example, your file should look like this:
(*
* This is my custom blocks implementation
*
*
* Test Block
* ———–
*)
(* This block should get an integer at I0, sum
10 to it and then output the sum at OUT
*)
FUNCTION_BLOCK TEST
VAR_INPUT
I0 : INT;
ACTIVE : BOOL;
END_VAR
VAR_OUTPUT
OUT : INT;
END_VAR
IF ACTIVE = TRUE THEN
OUT := I0 + 10;
END_IF;
END_FUNCTION_BLOCK
Save the file and then open standard_FB.txt located on the same folder. Include your new .txt file in it right below the last .txt inclusion at the end of the file. The end of your file should look similar to this:
(* Not in the standard, but useful nonetheless. *)
{#include “sema.txt” }
{#include “my_custom_library.txt” }{enable code generation}
Writing C code for your blocks
Locate the iec_std_FB.h file inside the MatIEC lib folder (it could be inside a “C” subfolder). Open it using a text editor. At the end of the file, right before the #endif statement, include a new .h file for your custom block library, so that the end of your file looks similar to this (your file could have different #include statements):
__end:
return;
} // SEMA_body__()#include “arduino_lib_FB.h”
#include “p1am_FB.h”
#include “communication.h”#include “my_custom_library.h”
#endif //_IEC_STD_FB_H
Create a new file “my_custom_library.h” on the same folder where ie_std_FB.h is located. This file will hold the C code for your custom block. Following MatIEC specifications, you must write the struct declaration, initialization and body for your block. To access the variables from your custom block you must use the MatIEC __GET_VAR() and __SET_VAR() macros. These macros takes several arguments and could be a bit complicated to use. In order to facilitate variable access, it is recommended to define new macros that facilitates __GET_VAR() and __SET_VAR() usage:
#define GetFbVar(var,…) __GET_VAR(data__->var,__VA_ARGS__)
#define SetFbVar(var,val,…) __SET_VAR(data__->,var,__VA_ARGS__,val)
Remember to #undef the macros once you’re done using them to avoid conflicts. For our particular example, your “my_custom_library.h” file should look like this:
//my_custom_library.h – this file contains the C code for the TEST block defined in the “My Custom Library”
// FUNCTION_BLOCK TEST
// Data part
typedef struct {
// FB Interface – IN, OUT, IN_OUT variables
__DECLARE_VAR(BOOL,EN)
__DECLARE_VAR(BOOL,ENO)
__DECLARE_VAR(INT,I0)
__DECLARE_VAR(BOOL,ACTIVE)
__DECLARE_VAR(INT,OUT)
} TEST;// Initialization part
static void TEST_init__(TEST *data__, BOOL retain) {
__INIT_VAR(data__->EN,__BOOL_LITERAL(TRUE),retain)
__INIT_VAR(data__->ENO,__BOOL_LITERAL(TRUE),retain)
__INIT_VAR(data__->I0,0,retain)
__INIT_VAR(data__->ACTIVE,__BOOL_LITERAL(FALSE),retain)
__INIT_VAR(data__->OUT,0,retain)
}// Code part
static void TEST_body__(TEST *data__) {// Control execution – this should be the same for every Function Block
if (!__GET_VAR(data__->EN)) {
__SET_VAR(data__->,ENO,,__BOOL_LITERAL(FALSE));
return;
}
else {
__SET_VAR(data__->,ENO,,__BOOL_LITERAL(TRUE));
}// Actual Code
#define GetFbVar(var,…) __GET_VAR(data__->var,__VA_ARGS__)
#define SetFbVar(var,val,…) __SET_VAR(data__->,var,__VA_ARGS__,val)
if (GetFbVar(ACTIVE)) {
SetFbVar(OUT, GetFbVar(I0) + 10);
}#undef GetFbVar
#undef SetFbVarreturn;
} // TEST_body__()
That’s it! Now your custom block is added to the editor and also to MatIEC and should compile just fine.
However, if the block you’re creating has Arduino-specific functionality in it, you cannot just write Arduino code in the my_custom_library.h file. Arduino was written in C++, where the PLC core was written in C. You must first create a bridge between both worlds so that the C code can call C++ functions and vice-versa. All Arduino C++ files are located at: OpenPLC_Editor/editor/arduino/examples/Baremetal. You can create your own .h file in there for your lib (making sure that it is also #included in Baremetal.ino) or just use the arduino_libs.h that is already there for this purpose.
Edit the arduino_libs.h file (or your own .h file) writing the Arduino code you would like to be added to your block. You must write this code in functions that can be called from the C part of the runtime. To be able to call C++ functions from C, all you have to do is prepend the function declaration with extern “C”. For example:
//Declare external C++/C function
extern “C” void print_number_on_serial(uint16_t num);bool first_time_called = true;
void print_number_on_serial(uint16_t num)
{
if (first_time_called)
{
//Setup Serial port as this is the first time this function is called
Serial.begin(115200);
first_time_called = false;
}
Serial.print(num);
}
This will make the “print_number_on_serial” function callable from your “my_custom_library.h” file in the C portion of the Runtime. For example, using our TEST block, we can call the print function to print the block output on Serial every time ACTIVE is TRUE. Our TEST block C code would have to be slightly modified:
// Define external C++/C function
// Since we are in C, we don’t have to prepend the function with extern “C”
void print_number_on_serial(uint16_t num);// Code part
static void TEST_body__(TEST *data__) {// Control execution – this should be the same for every Function Block
if (!__GET_VAR(data__->EN)) {
__SET_VAR(data__->,ENO,,__BOOL_LITERAL(FALSE));
return;
}
else {
__SET_VAR(data__->,ENO,,__BOOL_LITERAL(TRUE));
}// Actual Code
#define GetFbVar(var,…) __GET_VAR(data__->var,__VA_ARGS__)
#define SetFbVar(var,val,…) __SET_VAR(data__->,var,__VA_ARGS__,val)
if (GetFbVar(ACTIVE)) {
SetFbVar(OUT, GetFbVar(I0) + 10);
// Call the C++/C function to print on Serial
print_number_on_serial(GetFbVar(OUT));
}#undef GetFbVar
#undef SetFbVarreturn;
} // TEST_body__()