Creating FMU from OpenPLC build output with PythonFMU
The OpenPLC Editor is an IDE capable of creating programs for the OpenPLC Runtime, however it also has a graphical user interface tool for simulation/debugging of the PLC program.
When the PLC program simulation/debugging is executed in the IDE, the PLC program is compiled into a shared object (.so) binary file which contains the function pointers of setting and getting the traced variables.
In comparison, when the IDE generates PLC program for the runtime installed in various platforms, the end product of the compilation is a self-contained executable file, however the .so binary file used by the IDE simulation/debugging only contains the most minimalist function pointers for the traced variables. Therefore, for the purpose of generating FMUs, we would only need to use the .so binary file, which is why we're more interested in the simulation/debugging process and how to utilize the generated .so binary in the IDE.

The compilation in OpenPLC Editor IDE
The simulation/debugging in OpenPLC IDE
The execution flow of the OpenPLC Editor's simulation functionality (_Run method) can be analyzed in three main components:
- Runtime Loading: The system initiates PLC execution through
self._connector.StartPLC(), which interfaces with PLCObject.py to handle the dynamic loading of the shared object (.so) binary. This represents the compiled PLC program in memory. - Debug Infrastructure: The
self._connect_debug()method establishes the debugging framework by:- Initializing debug variable registration via
RegisterDebugVarToConnector(); - Implementing a timer-based mechanism for real-time simulation monitoring;
- Managing the IDE's debugging interface
- Initializing debug variable registration via
- Status Management: The system maintains state through
UpdateMethodsFromPLCStatus, which queries the PLC status viaself._connector.GetPLCstatus(). This creates a feedback loop between PLCObject.py and the IDE for runtime state monitoring.
However the OpenPLC Editor implements an on-demand debug variable registration system, the debug infrastructure uses a subscription model where variables are only registered when actively monitored through the GUI when a user clicks the debug instance button on a variable in the POUs. In order to trace all the variables during the simulation/debugging process at the same time, and execute the simulation/debugging process without the GUI, certain changes described bellow needs to be done.
Modifications to the OpenPLC IDE for FMU Generation
To extend the capabilities of the OpenPLC IDE, I added a new feature that generates an FMU-compatible wrapper script (FMUWrapper.py) whenever the "Start PLC Simulation" button is pressed. This wrapper script integrates seamlessly with the compiled .so binary of the PLC program, enabling the generation of a Functional Mock-up Unit (FMU) for model exchange and co-simulation.
Loading the PLC Binary
A crucial part of the FMUWrapper.py functionality involves dynamically loading the compiled .so binary of the PLC program. The binary, generated during the OpenPLC compilation process, contains the runtime logic of the PLC application.
The core function _LoadPLC is responsible for dynamic library loading, function pointers and debugging interfaces.
def _LoadPLC(self):
md5 = open(self._GetMD5FileName(), "r").read()
self.PLClibraryLock.acquire()
try:
self._PLClibraryHandle = dlopen(self._GetLibFileName())
self.PLClibraryHandle = ctypes.CDLL(self.CurrentPLCFilename, handle=self._PLClibraryHandle)
...
self._startPLC = self.PLClibraryHandle.startPLC
self._startPLC.restype = ctypes.c_int
self._startPLC.argtypes = [ctypes.c_int, ctypes.POINTER(ctypes.c_char_p)]
...
except Exception:
self._loading_error = traceback.format_exc()
return False
finally:
self.PLClibraryLock.release()
return TrueIt loads the .so binary file dynamically into the wrapper script using ctypes, and the dlopen function on Linux platforms facilitates this step. Function pointers are extracted (e.g., startPLC, stopPLC, RegisterDebugVariable) from the shared library and maps them to Python-callable functions, and it also initializes debugging-related functions, such as GetDebugData and RegisterDebugVariable, allowing real-time monitoring and interaction with the PLC logic.

1732792208820
Then we can generate the FMU by the command:
pythonfmu build -f FMUWrapper.py [projectname].soVariable registration
A core issue when creating FMUs is how to register variables in the FMU. Typically there are 3 types of variables in FMUs: input, output and local variables, and they need to be well-defined in the modelDescription.xml file in order to be used in the simulation platforms.
However, when the C files and VARIABLES.csv table are generated out of the PLC program during the compilation process, the information about the variable type is not explicitly maintained, making it challenging to register the variables in the FMU.
Fortunately, these variable category information exists in the plc.xml file used by the OpenPLC Editor IDE as the GUI notation for the PLC program. In order to address this issue, we need to implement an xml parser to extract the variable category information from the plc.xml file and then use this information to register the variables in the FMU.
Structure of plc.xml and the variable information
The plc.xml file is a tree-structured markup file used by the OpenPLC Ediror IDE which adheres to the PLCOpen XML format, designed for standardizing the representation of PLC programs, and in order to implement the parser to retrieve the information of the variables, we merely need to pay attention to several key elements in it.
Root Element: <project>
The <project> element encapsulates the entire PLC program's data. It includes attributes defining namespaces and contains child elements for metadata, data types, POU definitions, and instances.
Metadata Elements
<fileHeader> contains metadata such as the company name, product name, version, and creation date. It ensures traceability and documentation of the PLC program.
<fileHeader companyName="Beremiz" productName="Beremiz" productVersion="1" creationDateTime="2016-10-24T18:09:22"/><contentHeader> specifies details about the project content, including scaling information for different programming languages like FBD (Function Block Diagram), LD (Ladder Diagram), and SFC (Sequential Function Chart).
Types Section
The <types> element is a repository of user-defined data types and POUs (Program Organization Units).
Data Types (<dataTypes>) could be used for defining custom or derived data types.
POUs (<pous>): The <pous> element contains multiple <pou> definitions. Each <pou> represents a POU, categorized as:
function: Represents reusable logic.program: Contains the main execution logic.functionBlock: Defines encapsulated logic blocks with internal state.
For example, in the OpenPLC IDE there's a Multi_Language example PLC project, where the plc.xml contains the <pou> node of a program organization unit called "plc_prg", which is a program, the corresponding node is defined as follows:
<pou name="plc_prg" pouType="program">
<interface>
<inputVars>...</inputVars>
<outputVars>...</outputVars>
<localVars>...</localVars>
</interface>
<body>...</body>
</pou>Variable Definitions
Within each <pou>, the <interface> section defines:
- Input Variables (
<inputVars>) : Variables representing external inputs to the POU. - Output Variables (
<outputVars>) : Variables outputting data from the POU. - Local Variables (
<localVars>) : Internal variables scoped to the POU. - External Variables (
<externalVars>) : Variables referencing external global or constant values.
Each variable is defined with:
name: Identifier of the variable.type: Data type, such as<BOOL>or<INT>.initialValue(optional): Specifies a default value.
For example, the variables defined in a function block named CounterST has a structure as follows:
<interface>
<inputVars>
<variable name="Reset">
<type>
<BOOL/>
</type>
</variable>
</inputVars>
<localVars>
<variable name="Cnt">
<type>
<INT/>
</type>
</variable>
</localVars>
<outputVars>
<variable name="OUT">
<type>
<INT/>
</type>
</variable>
</outputVars>
<externalVars constant="true">
<variable name="ResetCounterValue">
<type>
<INT/>
</type>
</variable>
</externalVars>
</interface>Body Section
The <body> element specifies the logic implementation of a POU using one of the IEC 61131-3 languages (structual text, function block diagram, ladder diagram etc), each type of language implementation has it's own specific type of body element.
For example, a function block implemented in structural text would have a <ST> element that encodes logic in textual form.
<ST>
<xhtml:p><![CDATA[AverageVal := INT_TO_REAL(Cnt1+Cnt2+Cnt3+Cnt4+Cnt5)/InputsNumber;]]></xhtml:p>
</ST>Whereas a function block that has the same logic implemented by function block diagram would represent logic as interconnected blocks:
<FBD>
<block typeName="CounterST" instanceName="CounterST0">
<inputVariables>...</inputVariables>
</block>
</FBD>Instance Configuration
The <instances> element contains the runtime configuration, defining how POUs are instantiated and scheduled.
<instances>
<configurations>
<configuration name="config">
<resource name="Res0">
<task name="plc_task" priority="1" interval="T#100ms">
<pouInstance name="plc_task_instance" typeName="plc_prg"/>
</task>
</resource>
<globalVars constant="true">
<variable name="ResetCounterValue">
<type>
<INT/>
</type>
<initialValue>
<simpleValue value="17"/>
</initialValue>
</variable>
</globalVars>
</configuration>
</configurations>
</instances>In the <configurations> element of this example, each <configuration> element groups tasks and global variables under a named resource.
Variable information
{
"AverageVal": {
"input": {
"Cnt1": "INT",
"Cnt2": "INT",
"Cnt3": "INT",
"Cnt4": "INT",
"Cnt5": "INT",
},
"output": {},
"inout": {},
"local": {"InputsNumber": "REAL"},
"external": {},
},
"plc_prg": {
"input": {"Reset": "BOOL"},
"output": {
"Cnt1": "INT",
"Cnt2": "INT",
"Cnt3": "INT",
"Cnt4": "INT",
"Cnt5": "INT",
},
"inout": {},
"local": {
"CounterST0": "CounterST",
"CounterFBD0": "CounterFBD",
"CounterSFC0": "CounterSFC",
"CounterIL0": "CounterIL",
"CounterLD0": "CounterLD",
"AVCnt": "REAL",
},
"external": {},
},
"CounterST": {
"input": {"Reset": "BOOL"},
"output": {"OUT": "INT"},
"inout": {},
"local": {"Cnt": "INT"},
"external": {"ResetCounterValue": "INT"},
},
"CounterFBD": {
"input": {"Reset": "BOOL"},
"output": {"OUT": "INT"},
"inout": {},
"local": {"Cnt": "INT"},
"external": {"ResetCounterValue": "INT"},
},
"CounterSFC": {
"input": {"Reset": "BOOL"},
"output": {"OUT": "INT"},
"inout": {},
"local": {"Cnt": "INT"},
"external": {"ResetCounterValue": "INT"},
},
"CounterIL": {
"input": {"Reset": "BOOL"},
"output": {"OUT": "INT"},
"inout": {},
"local": {"Cnt": "INT"},
"external": {"ResetCounterValue": "INT"},
},
"CounterLD": {
"input": {"Reset": "BOOL"},
"output": {"Out": "INT"},
"inout": {},
"local": {"Cnt": "INT"},
"external": {"ResetCounterValue": "INT"},
},
}Structure of VARIABLES.csv and the variable information
A typical VARIABLES.csv file contains 3 sections labled by a line starting with //: Programs, Variables and Ticktime, the .csv file is generated by IEC2C compiler.
[
{
"num": "0",
"vartype": "VAR",
"IEC_path": "CONFIG.RESETCOUNTERVALUE",
"C_path": "CONFIG__RESETCOUNTERVALUE",
"type": "INT",
"derived": "INT",
"retain": "0",
},
{
"num": "1",
"vartype": "FB",
"IEC_path": "CONFIG.RES0.PLC_TASK_INSTANCE",
"C_path": "RES0__PLC_TASK_INSTANCE",
"type": "PLC_PRG",
"derived": "",
"retain": "0",
},
{
"num": "2",
"vartype": "VAR",
"IEC_path": "CONFIG.RES0.PLC_TASK_INSTANCE.RESET",
"C_path": "RES0__PLC_TASK_INSTANCE.RESET",
"type": "BOOL",
"derived": "BOOL",
"retain": "0",
},
{
"num": "3",
"vartype": "VAR",
"IEC_path": "CONFIG.RES0.PLC_TASK_INSTANCE.CNT1",
"C_path": "RES0__PLC_TASK_INSTANCE.CNT1",
"type": "INT",
"derived": "INT",
"retain": "0",
},
{
"num": "4",
"vartype": "VAR",
"IEC_path": "CONFIG.RES0.PLC_TASK_INSTANCE.CNT2",
"C_path": "RES0__PLC_TASK_INSTANCE.CNT2",
"type": "INT",
"derived": "INT",
"retain": "0",
},
{
"num": "5",
"vartype": "VAR",
"IEC_path": "CONFIG.RES0.PLC_TASK_INSTANCE.CNT3",
"C_path": "RES0__PLC_TASK_INSTANCE.CNT3",
"type": "INT",
"derived": "INT",
"retain": "0",
},
{
"num": "6",
"vartype": "VAR",
"IEC_path": "CONFIG.RES0.PLC_TASK_INSTANCE.CNT4",
"C_path": "RES0__PLC_TASK_INSTANCE.CNT4",
"type": "INT",
"derived": "INT",
"retain": "0",
},
{
"num": "7",
"vartype": "VAR",
"IEC_path": "CONFIG.RES0.PLC_TASK_INSTANCE.CNT5",
"C_path": "RES0__PLC_TASK_INSTANCE.CNT5",
"type": "INT",
"derived": "INT",
"retain": "0",
},
{
"num": "8",
"vartype": "FB",
"IEC_path": "CONFIG.RES0.PLC_TASK_INSTANCE.COUNTERST0",
"C_path": "RES0__PLC_TASK_INSTANCE.COUNTERST0",
"type": "COUNTERST",
"derived": "",
"retain": "0",
},
{
"num": "9",
"vartype": "VAR",
"IEC_path": "CONFIG.RES0.PLC_TASK_INSTANCE.COUNTERST0.EN",
"C_path": "RES0__PLC_TASK_INSTANCE.COUNTERST0.EN",
"type": "BOOL",
"derived": "BOOL",
"retain": "0",
},
.....
]Now the naming convention of the IEC_PATH is quite unclear from the output.
iec_types
parsing variable/pou connections from plc.xml
Since the generated VARIABLES.csv lost the information of the "connections" between the variables, another task is to parse the plc.xml directly and map the variables to the ones in VARIABLES.csv.
And in ProjectController.py the variables in VARIABLES.csv is already parsed into self._VariablesList in GetIECProgramsAndVariables(self), all of the xml variable mapping work is done in VariableMapper.py.
Example of generated FMUWrapper.py
here's one of the FMUWrapper.py generated from the plc program:
```python
def _register_fmu_variables(self):
# set traceList
self.traceIdxList.append((0, "SIN2TAN", None))
self.variable_unpack_list.append((0, "CONFIG0.RES0.INSTANCE0", "SIN2TAN"))
self.traceIdxList.append((1, "REAL", None))
self.variable_unpack_list.append((1, "CONFIG0.RES0.INSTANCE0.SINEVALUE", "REAL"))
self.traceIdxList.append((2, "REAL", None))
self.variable_unpack_list.append((2, "CONFIG0.RES0.INSTANCE0.TANGENTVALUE", "REAL"))
self.traceIdxList.append((3, "REAL", None))
self.variable_unpack_list.append((3, "CONFIG0.RES0.INSTANCE0.COSINEVALUE", "REAL"))
#register fmu variables
self.INSTANCE0__SINEVALUE = 0.0
self.fmuTraceVarList.append("INSTANCE0__SINEVALUE")
self.register_variable(Real("INSTANCE0__SINEVALUE", causality=Fmi2Causality.input, variability=Fmi2Variability.continuous, initial=Fmi2Initial.exact, start=0.0))
self.fmu_variable_unpack_list.append((1, "INSTANCE0__SINEVALUE", "REAL"))
self.INSTANCE0__TANGENTVALUE = 0.0
self.fmuTraceVarList.append("INSTANCE0__TANGENTVALUE")
self.register_variable(Real("INSTANCE0__TANGENTVALUE", causality=Fmi2Causality.output, variability=Fmi2Variability.continuous, initial=Fmi2Initial.calculated))
self.fmu_variable_unpack_list.append((2, "INSTANCE0__TANGENTVALUE", "REAL"))
self.INSTANCE0__COSINEVALUE = 0.0
self.fmuTraceVarList.append("INSTANCE0__COSINEVALUE")
self.register_variable(Real("INSTANCE0__COSINEVALUE", causality=Fmi2Causality.local, variability=Fmi2Variability.continuous, initial=Fmi2Initial.calculated))
self.fmu_variable_unpack_list.append((3, "INSTANCE0__COSINEVALUE", "REAL"))The trace list part is just the variables from `VARIABLES.csv`, and bellow are the variables mapped from both `plc.xml` and `VARIABLES.csv`.
### Generating FMU using PythonFMU
Eventually, I use a command to generate fmu out of `FMUWrapper.py` and other files like this:pythonfmu build -f FMUWrapper.py .so .dll lastbuildPLC.md5 requirements.txt runtime/ serial/
``
here the.soand.dllare the binaries built from PLC program, FMUWrapper is what generated from the build process (due to all my modifications to the IDE),runtime/is the openPLC IDE's implementation of the runtime used for interaction with the binaries, andserial/` is a third-party package required here.