Simple Extension Component

The simplest PLCnext component is a C++ class that inherits Arp::System::Acf::ComponentBase.

The C++ source files (.hpp and .cpp) for a simple component, called MyComponent, can be downloaded here:

At this point MyComponent is a valid PLCnext Runtime component, but it contains no functionality - this will be added later.

Library Singleton

Named component instances are created by the PLCnext runtime from a singleton that inherits Arp::System::Acf::LibraryBase.

The C++ source files (.hpp and .cpp) for a singleton - called MyLibrary - that creates named instances of MyComponent can be downloaded here:

Note that both MyComponent and MyLibrary are defined in a namespace called MyNamespace.

Building the Library

You can build a shared object library containing both these classes (MyComponent and MyLibrary) using the SDK that you installed earlier.

The following procedure uses the build tools CMake (version 3.19 or above) and Ninja, so make sure these are also installed on the host machine. The PLCnext CLI installs an older version of CMake which does not support the cmake-presets feature used in this procedure.

Build the library as follows:

  • On your host machine, create a project directory. Under this directory, create a sub-directory called src.

  • Copy the four source files to the src directory.

  • In the project root directory, create a CMakeLists.txt file containing the following text:

cmake_minimum_required(VERSION 3.13)

project(MyProject)

if(NOT CMAKE_BUILD_TYPE)
  set(CMAKE_BUILD_TYPE Release)
endif()

################# create target #######################################################

set (WILDCARD_SOURCE *.cpp)
set (WILDCARD_HEADER *.h *.hpp *.hxx)

file(GLOB_RECURSE Headers CONFIGURE_DEPENDS src/${WILDCARD_HEADER})
file(GLOB_RECURSE Sources CONFIGURE_DEPENDS src/${WILDCARD_SOURCE})
add_library(MyProject SHARED ${Headers} ${Sources})

#######################################################################################

################# project include-paths ###############################################

target_include_directories(MyProject
    PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>)

#######################################################################################

################# include arp cmake module path #######################################

list(INSERT CMAKE_MODULE_PATH 0 "${ARP_TOOLCHAIN_CMAKE_MODULE_PATH}")

#######################################################################################

################# set link options ####################################################
# WARNING: Without --no-undefined the linker will not check, whether all necessary    #
#          libraries are linked. When a library which is necessary is not linked,     #
#          the firmware will crash and there will be NO indication why it crashed.    #
#######################################################################################

target_link_options(MyProject PRIVATE LINKER:--no-undefined)

#######################################################################################

################# add link targets ####################################################

find_package(ArpDevice REQUIRED)
find_package(ArpProgramming REQUIRED)

target_link_libraries(MyProject PRIVATE ArpDevice ArpProgramming)

#######################################################################################

################# install ############################################################

string(REGEX REPLACE "^.*\\(([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+).*$" "\\1" _ARP_SHORT_DEVICE_VERSION ${ARP_DEVICE_VERSION})
install(TARGETS MyProject
    LIBRARY DESTINATION ${ARP_DEVICE}_${_ARP_SHORT_DEVICE_VERSION}/$<CONFIG>/lib
    ARCHIVE DESTINATION ${ARP_DEVICE}_${_ARP_SHORT_DEVICE_VERSION}/$<CONFIG>/lib
    RUNTIME DESTINATION ${ARP_DEVICE}_${_ARP_SHORT_DEVICE_VERSION}/$<CONFIG>/bin)
unset(_ARP_SHORT_DEVICE_VERSION)

#######################################################################################
  • Also in the project root directory, create a CMakePresets.json file containing the following text:
{
    "version": 3,
    "cmakeMinimumRequired": {
        "major": 3,
        "minor": 19,
        "patch": 0
    },
    "configurePresets": [
        {
            "name": "default",
            "hidden": true,
            "displayName": "Default Config",
            "description": "Default build using Ninja generator",
            "generator": "Ninja",
            "toolchainFile": "$env{PLCNEXT_SDK_ROOT}/toolchain.cmake",
            "binaryDir": "${sourceDir}/bin/$env{ARP_DEVICE}_$env{ARP_DEVICE_VERSION}",
            "installDir": "${sourceDir}/deploy",
            "cacheVariables": {
                "ARP_DEVICE": "$env{ARP_DEVICE}",
                "ARP_DEVICE_VERSION": "$env{ARP_DEVICE_VERSION}",
                "CMAKE_BUILD_WITH_INSTALL_RPATH": true
            }
        },
        {
            "name": "build-windows-AXCF2152-2021.9.0.40",
            "inherits": "default",
            "displayName": "AXCF2152-2021.9.0",
            "environment": {
                "ARP_DEVICE": "AXCF2152",
                "ARP_DEVICE_VERSION": "2021.9.0 (21.9.0.40)",
                "PLCNEXT_SDK_ROOT": "C:\\SDK\\AXCF2152\\2021.9"
            },
            "condition": {
                "type": "equals",
                "lhs": "${hostSystemName}",
                "rhs": "Windows"
            },
            "cacheVariables": {
                "CMAKE_TOOLCHAIN_FILE": {
                    "value": "$env{PLCNEXT_SDK_ROOT}/toolchain.cmake",
                    "type": "FILEPATH"
                }
            }
        },
        {
            "name": "build-linux-AXCF2152-2021.9.0.40",
            "inherits": "default",
            "displayName": "AXCF2152-2021.9.0",
            "environment": {
                "ARP_DEVICE": "AXCF2152",
                "ARP_DEVICE_VERSION": "2021.9.0 (21.9.0.40)",
                "PLCNEXT_SDK_ROOT": "/opt/pxc/sdk/AXCF2152/2021.9"
            },
            "condition": {
                "type": "equals",
                "lhs": "${hostSystemName}",
                "rhs": "Linux"
            }
        }
    ],
    "buildPresets": [
        {
            "name": "build-linux-AXCF2152-2021.9.0.40",
            "displayName": "AXCF2152-2021.9.0",
            "configurePreset": "build-linux-AXCF2152-2021.9.0.40"
        }
    ],
    "testPresets": [
        {
            "name": "default",
            "configurePreset": "default",
            "output": {
                "outputOnFailure": true
            },
            "execution": {
                "noTestsAction": "error",
                "stopOnFailure": true
            }
        }
    ]
}

You may need to change the ARP targets and SDK paths in this file to suit the setup of your development machine.

  • From the project root directory, configure, build and deploy the project:

    $ cmake --preset=build-linux-AXCF2152-2021.9.0.40
    
    $ cmake --build --preset=build-linux-AXCF2152-2021.9.0.40 --target all
    
    $ cmake --build --preset=build-linux-AXCF2152-2021.9.0.40 --target install
    

You will see from the output that a shared object library, libMyProject.so, has been created. This contains the component called MyNamespace::MyComponent and the singleton called MyNamespace::MyLibrary.

  • On the PLC, create a project directory e.g. /opt/plcnext/projects/MyProject, and a lib subdirectory.

  • Copy the shared object library from the host to the target:

    $ scp deploy/AXCF2152_21.9.0.40/Release/lib/libMyProject.so admin@192.168.1.10:~/projects/MyProject/lib
    

Instantiating the Component

Now that the shared object library containing the extension component is on the target, the PLCnext runtime must be instructed to create an instance of MyComponent.

  • On the host, in the project root directory, create a file named MyProject.acf.config, containing the following text:
<?xml version="1.0" encoding="UTF-8"?>
<AcfConfigurationDocument
  xmlns="http://www.phoenixcontact.com/schema/acfconfig"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.phoenixcontact.com/schema/acfconfig.xsd"
  schemaVersion="1.0" >

  <Processes>
    <Process name="MyProcess" settingsPath="$ARP_ACF_SETTINGS_FILE$" />
  </Processes>

  <Libraries>
    <Library name="MyProject" binaryPath="$ARP_PROJECTS_DIR$/MyProject/lib/libMyProject.so" />
  </Libraries>

  <Components>
    <Component name="MyComponentInstance" type="MyNamespace::MyComponent" library="MyProject" process="MyProcess" />
  </Components>

</AcfConfigurationDocument>
  • Copy this ACF configuration file from the host to the target:

    $ scp MyProject.acf.config admin@192.168.1.10:~/projects/Default
    

    The PLCnext runtime will automatically load this configuration file, since the Default.acf.config file in the same directory includes all files that match the pattern *.acf.config. The ACF configuration file for MyProject instructs the PLCnext runtime to:

    • Create a new child process called MyProcess.
    • Load the shared object library and name it MyProject. This name can be considered an alias, or shorthand reference, to the shared object library. This name does not have any relationship to the name of the class in the shared object library that inherited LibraryBase.
    • Create an instance of MyNamespace::MyComponent, called MyComponentInstance, from the library named MyProject, in the process named MyProcess.

    The process parameter is optional and, if omitted, the component instance will be created in the main PLCnext Runtime process (called MainProcess).

  • Restart the PLCnext runtime:

    # sudo /etc/init.d/plcnext restart && tail -f -n 0 /opt/plcnext/logs/Output.log
    (result)
    

Among the messages that appear in the Output.log file, you should see the following:

INFO  - Process 'MyProcess' started successfully.
INFO  - Library 'MyProject' in process 'MyProcess' loaded.
INFO  - Component 'MyComponentInstance' in process 'MyProcess' created.

Your first PLCnext runtime extension component instance is now running!