PLCnext on LinkedInPLCnext on Instagram  PLCnext on YouTube Github PLCnext CommunityStore PLCnext Community

 

 How to create a Blog Entry

Tutorial: Using the AXC F 2152 controller with Microsoft Azure

Written by: Damian Bombeeck
Date: 10 januari 2020


Summary

In this tutorial I will show you how to connect to- and use a cloud-based database while on your PLCnext AXCF 2152 controller. We will use Microsoft Azure as the cloud-service provider and perform queries using HTTP requests. These requests will be performed in a thread so they won’t affect the real-time system. I’ll go through the different steps to take and explain why these steps are needed, furthermore I’ll include all used code and the side-notes concerning the use of Azure with PLCnext and HTTP requests in general. The tutorial is mainly focused on C++, IEC61131-3, JSON and SQL and will not include languages as C#, python or Java.

 

As for the thread concerned, I will not be going into detail about how to set up a worker thread.
The PLCnext community has its own github page on which C++, C#, java and MQTT sample codes are available toghether with a short explanation. This includes sample codes for threading. To implement a thread, look at the github tutorial describing how to do so. I highly recommend you to take a look on there anyway!

Introduction

Connecting your device to a database can have multiple benefits. For example, you can store and retrieve predefined configurations, upload and download user data, share input/output data with other controllers and keep track of the status of controllers itself.

Of course, we want to use a database which is flexible and with as little maintenance as possible, this leads us to cloud-based databases provided through a cloud-service. One of these cloud-service providers is Microsoft Azure, which you may have heard of and offers a wide variety of cloud services and resources such as virtual networks, virtual machines, machine learning and many more, not just databases. Azure is a pay-as-you-go provider which lets you pay only for the services and resources you use and for the frequency you use them at, this means you can make it as cheap or as expensive as you’d like.

To keep the communication between Azure and the controller asynchronous and as simple as possible without multiple messages being sent back and forth during a single request for data, I chose to make use of HTTP. HTTP is a request response protocol for asynchronous communication, it enables you to sent a request for data or information (depending on the method) to an endpoint which in turn sends a response with the requested data or information. To be able to use this protocol i’ve installed libcurl on my controller. This a third-party open-source library which facilitates the functionality for making these requests.

NOTE: During the tutorial, some parts are left out, these parts already have a good tutorial written on them and this one is big enough as it is already. I will link these tutorials in the chapters where they are needed.

Prerequisites

The following hardware and software was used during the writing of this tutorial

Hardware:
Software:

NOTE: When using third-party software, it is the responsibility of the user to ensure that all license conditions are complied with.

Procedure Overview

Following is an overview of the steps that I will explain in this tutorial:

Step 1 - Setting up your database
  • Create the SQL database
  • Add contents
  • Create a stored procedure
  • Check contents & stored procedure
Step 2 - Setting up your Logic App
  • Create the workflow
  • Configure the workflow
Step 3 - Setting up the C++ code
  • Make a new PLCnext project
  • Add the C++ code
  • Add input/output ports
  • Build the project
Step 4 - Setting up PLCnext Engineer & the EHMI webserver
  • Configure the project
  • Create the IEC program
  • Link the input/output ports
  • Make a EHMI webserver
Step 5 - Test the functionality
  • Test method
  • Debug methods
Appendix - Used codes
  • C++ codes
  • CMake codes
  • IEC codes

Procedure


Step 1: Setting up your database

We will kick the tutorial off with creating a SQL database to which we can add contents.

NOTE: Please keep in mind that Microsoft is still busy developing Azure and its functionality, some options may not be the same anymore opposed to when the tutorial was written.

Create the SQL database

We start with a basic Azure account without any resources or services in use, next navigate in the menu-bar on the left and choose SQL databases. This wil open an overview with the current existing databases. Choose the Add option and the following window will open.

create_sql_database

Fill in the fields and choose your current resource group in the drop-down menu. At the database details, fill in a database name which is easy recognizable and next choose Create new for a new SQL server. This will open the window depicted below.

create_sql_server

Again, choose a name that is easy recognizable. Fill in the rest of the form and complete the creation by clicking OK. Remember your credentials, you will need them later on in the tutorial. Next, choose no for the elastic pool. An elastic pool consists of multiple databases with different sizes and use intensities, we will be using a single database so this option is not applicable to us.

At the bottem, the Compute + storage option let’s us choose the performance we want to achieve with our database. When clicking on configure database a new window will open. With this option you are free to choose whatever suits your needs, it won’t affect this tutorial. Do keep in mind that the more advanced you want the database to be, the more you will have pay for it. On the right you can keep track of the costs a month with the chosen performance tier.

create_sql_database_DTU

I chose to use the Basic tier with 5 DTU’s and 2Gb of storage which ammounts to €4.21 a month. The meaning of the word DTU is quite vaque, it stands for Database Throughput Unit and according to the Azure documentation DTUs provide a way to describe the relative capacity of a performance level of Basic, Standard, and Premium tier databases. They are based on a blended measure of CPU, memory, reads and writes. As DTUs increase, the power offered by the performance level increases.

create_sql_database_TIER

When everything is filled in correctly, click on Next: Networking and configure your network settings. Make sure that Connectivity method is set to public endpoint and that Allow Azure services and resources to access this server and Add current client IP address are set to yes. These options can also be configured after the database has been created.

create_sql_database_networking

The following tabs Additional settings and Tags stay unaltered and can be left as they are. The last tab is Review + create which let’s you see your chosen configuration. If everything is correct, click Create to deploy your database.

create_sql_database_summary

Add contents

Now that we have our database and server up and running, we can add some contents to work with for the remainder of the tutorial. Navigate to your database instance and choose the Query editor (preview) from the menu on the left.

query_editor_login

We are required to log in before we can make any changes. Remember the credentials you filled in when making the SQL server? It’s time to use those credentials. If you are unable to log in because of denied access, click on Set server firewall ([server_name]) which will take you to the firewall configuration of your server.

query_editor_login_fail

Here you can make new firewall rules to make sure your IP address has access privilages. You can either enter your IP adress manually or let Azure do it automatically by choosing + Add client IP. Be sure to check that Allow services and resources to access this server is enabled. Save the configuration and try to log in again.

firewall_settings

If you succesfully added your IP adress to the firewall and are able to log in, the next window will open. This window lets us use SQL (Structured query language) and execute queries to alter the database. Write the SQL statement shown below into the query editor and execute the query by clicking on the Run button.

CREATE TABLE persons (
    PersonID int,
    PersonName varchar(255));

After execution the message Query succeeded: Affected rows: 0 wil show at the bottom. We can check to make sure a table named persons with the columns PersonID and PersonName was created. Refresh the editor and expand the tables tab in the drop-down menu, this should now show your created table and its columns.

execute_insert_query

We now have a database with a table and some columns, but those are still empty. To actually put some data into the table we have to execute another query. Insert the statement below into the editor and execute it.

INSERT INTO persons (PersonID, PersonName) 
VALUES ('1', 'Damian'); 

This will insert the number 1 into column PersonID and the name Damian into column PersonName within the table persons. If the query was executed succesfully the message Query succeeded: Affected rows: 1 will show at the bottom. Execute this query a few times with different values to add more data to the database.

An alternative way to add data is through the Edit data (preview) option. You can find this option after a table is created. Expand the tables tab and after that the dbo.persons tab in the tree menu, click on the dots on the right and there you will find this option.

edit_data_(preview)

Before the new editor opens, you first have to agree with some terms. Check the checkbox and click on OK. The editor will now open which visually shows the database with its columns and rows. You can add rows with Create New Row or alter existing rows by clicking on them.

edit_data_(preview)_overview

Create a stored procedure

Because we want to be able to search for data through our database, we are going to create a so called stored procedure. At the bottom of the drop-down menu you can view all stored procedures, at the moment only the system stored procedures exist.

menu_bar_extended_stored_procedure

With the query below we can add a new one. In this query we define the name of the procedure, the variable we want to use for searching and the SQL statement we want to execute. We declare the variable to be of type char and is max 30 characters long. We link this variable to the column we want to search through with the line PersonName = @name. The SQL statement you want to execute needs to be typed between AS and GO, at the moment this is a SELECT statement, but this could be any SQL statement you want. With the SELECT statement, we select every row from the column PersonName which complies with the value of search variable @name. Type the query below in the editor and execute it.

CREATE PROCEDURE seekElement @name nvarchar(30)
AS
    SELECT * FROM persons WHERE PersonName = @name
GO;

create_stored_procedure

The * sign is a wildcard character, this means the SELECT statement will select any and all data it finds which are the same as the value of the search variable. If everything went according to plan we should now have one custom stored procedure that can be used by other resources.

show_created_stored_procedure

Check contents & stored procedure

To check the content of the database we can execute the SELECT statement. Type in the following statement in the editor and execute it. This will return all the contents of the table persons in the results at the bottom.

SELECT * FROM persons; 

We can also check the functionality of the stored procedure. We do this by executing the procedure and giving the variable @name a value. If the search variable exists in the table, it will return the row with the corresponding value.

EXEC seekElement @name = "Damian"

test_stored_procedure

Step 2: Setting up your Logic App

To be able to use the database remotely we are going to implement a workflow with the Logic App resource from Azure. A workflow is a sequence of actions/steps and is executed from initiation till the end of the workflow and its actions/steps. It is a repeatable proces which can be initiated by a trigger and this trigger can be anything like incoming data, a received mail, an updata of the database, new RSS feeds etc.

Create the workflow

Start by searching in the top search box for Logic app. Once found, select the resource to open it, this wil open an overview with the current logic apps. Choose the Add option and the following window will open.

logic_app_search

Give your logic app a recognizable name, select your subscription and choose Use existing for the resource group. Make sure that you choose a location closest to you for the best performance. After this click on Create, the window will close and you will be brought back to the Logic App overview. Refresh the overview to see your newly made logic app, click on it to open the resource. This will immediately open the Logic app designer, which will hapen only the first time.

This starting window shows a list of common triggers and much used workflow templates. Select the When a HTTP request is received trigger to start designing your workflow. You will be presented with a opened trigger ready for configuration.

choose_trigger

trigger_overview

Next, click on + New step to add an action to the workflow. This will open up a window which lets you search through all the usable actions. If an action is unusable in the workflow, it won’t be shown in this list. As mentioned before, we will make us of the stored procedure so we have to choose the Execute stored procedure (V2) action.

 stored_procedure_action

The last action/step we need to add is the HTTP response to send the results from the stored procedure back to the C++ code. Again, click on + New step and search for response in the search box.

response_action

That’s all for the structure of the workflow. You should now have three steps consisting of one trigger and two actions. The finished workflow should look like this:

workflow_overview

You can expand this workflow if you’d like and add other actions. Thats the nice thing about this service, its very versatile and you are able to design it to your needs. In this tutorial we use a HTTP trigger to reach the SQL database, but you could also reach other resources of Azure in this manner or use different triggers to execute the workflow. For example, if you want to receive an Email notification when someone retrieves information, you can add a parallel branch with a new step and assign an office 365 action like Send an email (V2). Just keep your mouse in between two actions/steps and a blue + sign will appear which lets you add add a new step between the two or lets you make a parallel branch to which you can add new actions.

add_parallel_step

Configure the workflow

The next thing we have to do is configure the Execute stored procedure (V2) and Response action, our trigger is already configured correctly.

For the stored procedure action we must link our database and table to the workflow action, such a connection is called an API connection and will be visable as a resource once created. Select SQL server authentication in the drop-down menu of the stored procedure action. The window will expand showing a form.

sql_connector_config

Fill in the form according to the names you’ve given your SQL database and server, the full server name can be found at the resource itself. Fill in the credentials you’ve made during the creation of the server, choose your subscription and select + install gateway for the connection gateway. This will immediately open up a website with the Azure documentation, you can close this and click on Create to finish the setup.

The stored procedure action window will look different now. You can choose your server name, database name and procedure name. Because we configured the API connector, we can just click on the drop-down menu’s and select the Use connection settings option. For the procedure name, choose the created procedure named [dbo].[seekElement].

stored_procedure_overview_filled_in

Save the workflow and go back to your dashboard, go back to your workflow again and open the Logic App designer. We do this to give the workflow the change to update its variables. If we would not do this, we wouldn’t be able to use the variables from the output of the trigger.

We can now add our variable to use for the stored procedure. The biggest issue concerning this is that the syntax for an SQL variable is denoted with an @ sign, but JSON does not allow us to begin a key with this sign. So "@name": "Damian" is not allowed. To solve this we are going to create two JSON pairs in our C++ code, each pair is assigned to a variable and each pair contains a part of the key/value pair we want to end up with.

variable1 = "search_pointer: @name";      //pair one, containing the needed key "@name"
variable2 = "search_variable: Damian";    //pair two, containing the needed value "Damian"

NOTE: I use the name/value Damian as an example for the search variable, this variable could be any name/value you would like.

These pairs/variables are passed to the workflow during the HTTP request using headers. This will be discussed when adding the C++ code. We can take out the values @name and Damian of these JSON pairs and these values are then put together as one new JSON pair.

"@name": "Damian"

Click on the drop-down menu Add new parameter, this will show Raw inputs as the only option. Select it and a new input line will appear.

stored_procedure_raw_inputs

stored_procedure_raw_inputs_line

Next we are going to add a body to the raw input. The most important thing to know is that this input only accepts valid JSON pairs and objects, else you won’t be able to save the workflow. Start by entering two accolades.This creates the beginning of the JSON object, which is always between accolades. Stand on the empty line between the accolades and type a quotation mark, then click on the option Add dynamic content to add variable content to your body. Click on Expression on the right and scroll down to Referencing functions. Select the option triggerOutputs(), this will show the function at the top which enables you to edit it.

stored_procedure_raw_inputs-dynamic_content

stored_procedure_triggerOutputs_dynamic_content

triggerOutputs_syntax

We need to first specify that we want to use the the headers which are sent along with the HTTP request. We do this by appending ['headers'] behind the function. After that we specify which key/value JSON pair we want to use from these headers, append ['search_pointer'] to choose the key from the first C++ variable which contains the key/value pair with the value "@name". The workflow automatically ignores the key and only uses the value attached to it.

triggerOutputs()['headers']['search_pointer']

stored_procedure_triggerOutputs_total_line

After you’ve entered the line specifying the headers, click on OK to add the variable to your raw input. Enter a quotation mark again to finish the key.

stored_procedure_raw_inputs_body_1

We now have a key and need to add a value to that key to finish the JSON pair. Enter a colon and after that a space, click on Add dynamic content and do the same as before. This time we specify the key from the second C++ variable search_variable which contains the key/value pair with the value Damian attached to it. Again, the key is ignored and only the value attached to it will be used.

triggerOutputs()['headers']['search_variable']  

The raw input is complete and should look like this. It is a valid JSON object which will extract the values from both keys in the C++ variables containing the JSON pairs and turn them into a new single key/value JSON pair.

stored_procedure_raw_inputs_body_2

The results from the stored procedure needs to be sent back using the HTTP response. We are going to add content to the body of this last action/step. Click on the input line and choose Add dynamic content, we are presented with a list of outputs from the stored procedure. Choose the output ResultSets Table1, this will return only the tables of the results from the stored procedure and will leave out any other information like the statuscode or parameters.

stored_procedure_response

Save your finished workflow again and you are finished configuring Azure. One important thing to note is that you should not alter the workflow from now on or you will have to add the raw inputs again. When the workflow is saved and opened again, the Logic App designer implements the key of the JSON pair into the form, so it won’t be variable anymore and will just display the bare syntax @triggerOutputs()['headers'] as the key.

workflow_broken

Step 3: Setting up the C++ code

Now that we have our SQL database configured in the cloud, we can start focussing on reaching this service using the AXCF 2152 controller. We need to create a C++ code for the controller which executes the HTTP requests and passes the incoming and outcoming data to a IEC program. We will create this IEC program in step 4, it facilitates the user input and the communication between the GUI (graphical user interface) and the C++ program.

NOTE: These requests will be executed within a thread, the implementation of this thread will not be shown. Please look at the summary for the link to the github tutorial.

Make a new PLCnext project

First we have to prepare our environment on which we are going to create our C++ code. I chose to use a Linux system, Ubuntu 18.04.3 in particular. The reason I chose Linux is the ease of use when working with it, but before we can continue using this environment we first need to install the PLCnext SDK. We use the SDK (software development kit) to set up and build our project specifically for the PLCnext controller. For more information on setting up a PLCnext C++ project look at the guides on the PLCnext community

Download the PLCnext SDK for Linux 64bit, watch out for the version you are downloading. After you have downloaded the SDK, install it using the shell script, do this by entering the following command in your terminal:

Linux-PC$ sudo ./pxc-glibc-x86_64-axcf2152-image-sdk-cortexa9t2hf-neon-toolchain-2019.0.sh -d /usr/programs/pxc/sdk/AXCF2152/2019.04

The option -d is the directory you want the SDK to be installed to. Choose a directory fitting to your needs. After that, create a new folder named Azure_connection for your PLCnext project. Navigate to this folder and open a terminal within it. Using the SDK to create the project, enter the commands below.

Linux-PC$ plcncli new project --component MyComponent --name "Azure_connection_cpp" --program MyProgram --ouput /home/damian/Desktop/Azure_connection
Linux-PC$ plcncli set target --add --version 19.0.4.22338 --name AXCF2152 --path /home/damian/Desktop/Azure_connection 

NOTE: If you can’t execute the shell script, Right-click on the file and open the properties. Navigate to the Permissions window and make sure the Execute box at the bottom is checked.

the --output option in the first command is the path to the directory you’ve just made. I’ve made mine on my desktop so the path to this folder is /home/damian/Desktop/Azure_connection. The --version option in the second command is the firmware version you are using on your controller. If you are using a different firmware version, make sure you specify this version here.

If you don’t know which version is running on your controller, go to your internet browser and type in the IP address of your controller followed by wbm. My controller uses the IP address 192.168.6.49, so it would be 192.168.6.49/wbm/.

This will open up the wbm (web-based manager) of your specific controller. Log in using the name and password of the controller and you will be redirected. You will find the firmware version in the General Data tab. Keep in mind that this method only works while your controller is connected to the local network and has a known IP address

WBM_firmware

We should now have a folder named Azure_connection and within that folder the PLCnext project named Azure_connection_cpp consisting of multiple files and the source directory.

new_project_contents

Within the src directory you will find the header and source files for the program and component. We won’t be working in the MyProgram.cpp or MyProgram.hpp and only focus on the MyComponent.cpp and MyComponent.hpp.

Add the C++ code

Our project is all set up and we can begin to add code to the project. The files in the “src” directory already contain code, leave this be as much as you can and only change what you know and what is necessary. The created source code also facilitates system functions, not just the functions you implement, breaking one of these system functions might result in the controller not functioning.

Navigate to the src directory, open a terminal and type in the command shown below.

Linux-PC$ code MyComponent.hpp MyComponent.cpp 

NOTE: Don’t forget to include the #include "curl.h" header for the curl libray and the #include <string> header for the use of strings!

This will open up visual studio code with the MyComponent.hpp and MyComponent.cpp opened. I chose to write my code using Visual Studio Code, but you can use any text editor like Eclipse, notepad or Atom for the following steps.

We will begin in the header file. It is constructed of a class with multiple inheritances and within this class there are public and private function declarations. Scroll down to the static factory operations which are public functions. Add the new function write_data() below the other static function. This function is used to write the incoming data from the HTTP request to a string buffer.

static size_t write_data(void* ptr, size_t size, size_t nmemb, void* userp); 

write_data_declaration

After you’ve added this line, go to the MyComponent.cpp and scroll down. We declared the function in the header file and are going to implement the functionality in the source file. Insert the code shown below as a new function.

size_t MyComponent::write_data(void* ptr, size_t size, size_t nmemb, void* userp)
{
     ((std::string*)userp)->append((char*)ptr, size * nmemb);
     return size * nmemb;
} 

write_data_function

That is all for the write function, we can now begin with the thread body. If you’ve followed the tutorial on github, you should have a function declaration called workerThreadBody(void) in the header file and a function body called void MyComponent::workerThreadBody(void) in the source file. This last one is your thread body, everything that the thread needs to execute should be put here and It should look like this:

/// Thread Body
void MyComponent::workerThreadBody(void) 
{   
        const char* input_search_variable = reinterpret_cast<char*>(search_var_byte);
        const char* var_syntax = "search_variable: ";
        char variable_buffer[256];

        strncpy(variable_buffer, var_syntax, sizeof(variable_buffer));
        strncat(variable_buffer, input_search_variable, sizeof(variable_buffer));

        if (start_download_input == true)
        {
            struct curl_slist* headers = NULL;
            CURL* curl;
            CURLcode res;

            const char* url = "[the url of your endpoint/trigger]";
            const char* Content_Type = "Content-Type: application/json";
            const char* Content_Length_H = "Content-Length: 0";
            const char* search_pointer = "search_pointer: @name";
            const std::string HTTP_buffer;
            headers = NULL;

            curl = curl_easy_init();
            if (curl)
            {
                curl_easy_setopt(curl, CURLOPT_URL, url);
                curl_easy_setopt(curl, CURLOPT_POST, 1L);

                headers = curl_slist_append(headers, Content_Type);
                headers = curl_slist_append(headers, Content_Length_H);
                headers = curl_slist_append(headers, Search_pointer);
                headers = curl_slist_append(headers, variable_buffer);

                curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
                curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_data);
                curl_easy_setopt(curl, CURLOPT_WRITEDATA, &HTTP_buffer);

                res = curl_easy_perform(curl);
                curl_easy_cleanup(curl);
                curl_slist_free_all(headers);
            } 

            sizeof_result_string = HTTP_buffer.size(); 
            for (int i = 0; i <= sizeof_result_string; i++)
            {
                result_string_byte[i] = HTTP_buffer[i]; 
            }
            start_buffer = true;
            start_download_output = true;       
        }

        if(start_download_input == false)
        {
            start_buffer = false;
            start_download_output = false;            
        }
}

In the code I first create a buffer and two variables, this part is to append the search variable to the JSON pair i’m going to pass to Azure using headers. Remember the two JSON pairs we broke apart in the workflow to create a new key/value pair? The variables search_pointer and variable_buffer contain these JSON pairs we break apart in the workflow.

I take the input from the IEC program and transfer it to my buffer using the strncpy() and strncat() functions. After that I start my IF statement, which will be executed if the variable start_download_input equals true. This variable is passed from the IEC program to the C++ program and is an user input.

In my IF statement I start with declaring my variables. I begin with a struct to contain the headers I am going to send along with the request. After that come the variables, Content_Length and Content_Type are mandatory for a POST request in Azure and must not be removed or altered. The most important variable that MUST be altered is url, this is the URL of your endpoint and can be found in the workflow of your Logic App. Open the Logic App designer and click on the trigger. This wil expand it and at the top of the window you will find the URL needed to reach the trigger.

trigger_POST_URL

Next I create a handle for curl to use with the curl_easy_setopt options to set up the HTTP request. I do this with the curl_easy_init() function and link the handle to the variable curl. I then make a IF statement which will only be executed when the handle is created and exists. Within this statement I call the curl_easy_setopt() function, this function enables me to configure the behaviour of the request with all the necessary options. In the function I first specify on which handle the options must take effect, which is curl. I then declare the option I want to alter and lastly I specify which value or function (depending on the chosen option) I want to give the option. The meaning of the different options can be found here. Notice that one of the curl_easy_setopt() functions uses the write_data function we added earlier. Furthermore I add the headers to my struct using the curl_slist_append() function and at the end of the IF statement I free my handle using the curl_easy_cleanup() function and empty my struct using the curl_slist_free_all() function.

After I have closed my IF statement, the HTTP request has been executed and the HTTP_buffer variable contains the results from the database in the form of a string. I can now request the size of the string containing my results using the size() function. This information is necessary to transfer the results to the IEC program, as I use it in my FOR statement. I use a FOR statement with a ranged loop to transfer the result to a buffer variable named result_string_byte of the type byte array. This enables me to send the string to my IEC program as a byte array filled with individual ascii-characters. The reason I do this is because the IEC program does not recognize a C++ string type, I first have to convert the string to an array of bytes, then pass that array over and covert it back to a IEC string in my IEC program.

The last IF statement in the code is to reset the variables. As soon as the user input becomes false or the request has been executed the start_download_output and start_buffer variables become false, stopping the variables and buffers from changing.

Add input/output ports

Next, we are going to declare our ports. These ports are used to pass data between the C++ and IEC program such as the search variable the user enters and the results from the database which are returned.

Open the MyComponent.hpp, scroll down to the public port declarations and add the following pieces of code to declare the ports. I’ve added an explanation above the code as to what the port is going to be used for.

This port is used to pass the user input from the GUI and IEC program to the C++ program which is used to start the execution of the HTTP request.

//#port
//#attributes(Input)
//#name(start_download_input)
bool start_download_input = false;

This port is used to pass the status of the request to the IEC program, this is necessary to make sure the request is only executed once and won’t keep on executing. After the execution is started and completed, this variable will become true, so the IEC program knows it has executed the request.

//#port
//#attributes(Output)
//#name(start_download_output)
bool start_download_output = false;

The data we pass between the C++ and IEC program is often of the type string, the problem is that a string type in C++ is different then a string type in IEC. To solve this, we use a buffer function which devides/converts the string into individual ascii-characters of type char and writes these to a buffer. That buffer is then passed to the C++ program. We use this port and variable to signal that the conversion process in the IEC program can start.

//#port
//#attributes(Output)
//#name(start_buffer) 
bool start_buffer = false;

As mentioned before, the results from the database that are returned are of type string and can only be passed to the IEC program using a buffer. The buffer we are talking about is created here with this port. The buffer is a simple byte array consisting of maximal 200 bytes, each byte representing one ascii-character.

//#port
//#attributes(Output)
//#name(result_string_byte)
uint8 result_string_byte[200];

The search variable from the input of the user that we pass to the C++ program is of type string and is first converted to a buffer as a byte array in the IEC program. We need a buffer in our C++ code to be able to receive the array of bytes from the IEC program. That is what this input buffer is for. The buffer is a simple byte array consisting of maximal 200 bytes, each byte representing one ascii-character.

//#port
//#attributes(Input)
//#name(search_var_byte)
uint8 search_var_byte[200];

To convert a series of elements from a buffer back to a string in the IEC program, we need to know how many elements are in the buffer. To find this out, I ask for the size of the string (containing the results from the database) in my C++ code using the size() function and send this size to the IEC program. This way the IEC program now knows the number of elements and can start the conversion from the buffer to a string.

//#port
//#attributes(Output)
//#name(sizeof_result_string)
int16 sizeof_result_string = 0;

After you’ve added all the ports, save the header file and close it. We are done with the C++ code for this tutorial and can start building the project.

Build the project

For this next part of the tutorial it is very important that you know how to cross-compile a library from source files, as we need the curl library built for an ARM architecture. If you follow my other tutorial you should manage to do this and end up with a libcurl.so file. Alternatively, you could go to my github where I uploaded all project files to and download the files from there.

In the project directory on my Linux pc I’ve made two new folders. One folder is named external and contains the cross-compiled libcurl library, the other folder is named cmake and contains a cmake module to find this library. I expect you to have the external folder with the built library ready, I’m only going to show you how to edit the CMakeListst.txt and create the cmake module.

While in the root of your project, open the CMakeListst.txt and scroll down to the include arp cmake module path part, insert the following line of code at the bottom of that part.

list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") 

This line serves to find the cmake module in the cmake folder and use that module to find the library files needed to built the project. Next, scroll down to the part add link targets and add the following line below it.

find_package(CURL REQUIRED)

below that line you see a line that starts with target_link_libraries..., add curl::curl at the end of this line. It should look like the following line:

target_link_libraries(Azure_connection_cpp PRIVATE ArpDevice ArpProgramming curl::curl)

You can then save and close this file. Navigate to the newly made cmake folder and make a new text file by opening a terminal and entering code. Visual Studio Code will open at the start menu and you need to choose New file to open an empty file. Add the code below to it and save the file as findCURL.cmake. You can name it whatever you want, just make sure it has the .cmake filename extension.

set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE BOTH)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY BOTH)

find_path(CURL_INCLUDE_DIR
    NAMES curl.h
    PATHS external/curl_build_ARM/include/curl
)
find_library(CURL_LIBRARY
    NAMES curl
    PATHS external/curl_build_ARM/lib
)

include(FindPackageHandleStandardArgs)

find_package_handle_standard_args(curl
    DEFAULT_MSG
    CURL_INCLUDE_DIR CURL_LIBRARY
)

if(CURL_FOUND)
    set(CURL_LIBRARIES ${CURL_LIBRARY})
    set(CURL_INCLUDE_DIRS ${CURL_INCLUDE_DIR})
endif()

if(CURL_FOUND AND NOT TARGET curl::curl)
    add_library(curl::curl UNKNOWN IMPORTED)
    set_target_properties(curl::curl PROPERTIES
        IMPORTED_LOCATION "${CURL_LIBRARY}"
        INTERFACE_INCLUDE_DIRECTORIES "${CURL_INCLUDE_DIR}"
    )
endif()

mark_as_advanced(
    CURL_INCLUDE_DIR CURL_INCLUDE_DIRS
    CURL_LIBRARY CURL_LIBRARIES)

There are two lines in this code that need attention. The first one is the PATHS in find_path and the second one is the PATHS in find_library. The first one should be the path to your include file curl.h, the second one should be the path to your shared library file libcurl.so. If you followed along and chose the same names as me, you can leave this code as it is. If not, make sure to check that the paths are set correctly. You can leave the rest of the code as it is. For more information, look at this tutorial about including open-source libraries in your C++ project.

We now have the project all set up and ready to be build. Open a terminal and enter the following commands one after each other. Keep in mind that the option -p specifies your project folder and is different for everyone, in my case it’s /home/damian/Desktop/Azure_connection.

Linux-PC$ sudo plcncli generate code -p /home/damian/Desktop/Azure_connection/ --sources "src"

generate_code_project

Linux-PC$ sudo plcncli generate config -p /home/damian/Desktop/Azure_connection/ --sources "src"

generate_config_project

Linux-PC$ sudo plcncli build -p /home/damian/Desktop/Azure_connection/ -b Release 

build_project

Linux-PC$ sudo plcncli generate library -p /home/damian/Desktop/Azure_connection/ --sources "src"

generate_library_project

After you’ve done this, you should be left with a few more folders inside your project root directory. One of these folders’s is the bin folder, navigate to this folder and you will see another folder and one file. The file, named after your project and of the type .pcwlx, is the one we are going to need and can be seen as a library.

created_library

This file contains the entire project and the C++ program you just built. Copy this file to your PC which has PLCnext Engineer installed on it and we are ready to move on to the next step.

Step 4: Setting up PLCnext Engineer & the EHMI webserver

The basic functionality for the user input and visual representation of the returned data is realized using a IEC program. This program is linked to the C++ code with ports and linked to the EHMI webserver with variables using special tags.

Configure the project

Start by opening PLCnext engineer, I use version 2019.6. At the welcome screen, in the tab Try one of our sample projects, choose the empty project for version 2019.0.0. I’m not going to go over the details of setting up the project. For more information on this, take a look at one of these tutorials.

Next, on the right hand side in the components pane expand the tab Libraries. Right-click on the folder that shows up and click on Add User Library.... We are going to import our created library from step 3, selecht the .pcwlx file and click on open. After that, expand the tab PLCnext Components & Programs under te programming tab and keep on expanding the tabs untill you see your program called MyProgram.

adding_library_file

Drag the program to ESM1 under the cyclic100 task to add it to your project. By doing this we have made the C++ code part of our PLCnext project.

ESM1

We also need to add two custom data-types for the IEC program. Remember the buffers we made when we added our ports? These buffers could hold a max of 200 bytes, which was specified by me. The max length of a IEC string is 80 bytes, I find this to be on the low side and it could result in missing parts of the returned results if the length of the results exceeds the 80 bytes.

To solve this, I make a custom string data type which is specified to hold a max of 200 bytes. This is done to make sure no parts of the returned results are lost. The other custom data type we are going to make is a byte array to store the user input to. The search variable coming from the user input needs to be passed over to the C++ code as individual ascii-characters, IEC does not include byte arrays big enough to make such a buffer so we have to implement this ourselves.

On the right-hand side under components expand the tab programming, then expand the tab Local. Right-click on the tab Data Types and click on Add Data Type Worksheet. This will add a worksheet below the tab.

datatypes_worksheet_tab

Open this worksheet by double-clicking and a file should open with no types declared.
Insert the following code between TYPE and END_TYPE.

byteArray200 : ARRAY[0 .. 199] OF BYTE; 
String200: STRING[200]; 

datatypes_worksheet_code

We first declare the name of the type, then declare the type itself by using a existing data type or structure and lastly specify the type to our needs. For the byte array we choose the data structure array with a specified range from 0 to 199 and of the data type byte. For the string we use the data type string with a max specified amount of 200 ascii-characters.

Save the worksheet and close it as we don’t need to alter this anymore. Now that we have our project configured, our C++ code imported and the custom data types set up we can begin with writing the IEC program.

Create the IEC program

Start by expanding the tab programming and Local again, then expand the tab Programs. You will see a program called Main, double-click on this and the start-screen should open which lets you choose the language for programming.

choose_IEC_language

Choose for the option Add ST Code Worksheet and the screen changes to the tab with your variables, which is empty ofcourse. Click on the tab Code at the top and a empty text file will open. Insert the following code.

IF (start_download_input_cpp = TRUE) THEN
    start_download_output_IEC := FALSE;
END_IF

IF (search_button = TRUE) THEN
    xStart_String_to_Buf := TRUE;
END_IF

IF (xDone_String_to_Buf = TRUE) THEN
    xStart_String_to_Buf := FALSE;
    start_download_output_IEC := TRUE;
END_IF

sizeof_search_variable := TO_UDINT(LEN(input_search_variable));

BUF_TO_STRING1
(
    REQ := xStart_Buf_to_String, 
    BUF_CNT := TO_DINT(sizeof_result_string_IEC), 
    DONE => xDone_Buf_to_String, 
    BUFFER := result_string_byte_IEC, 
    DST := Result_string200
);   

STRING_TO_BUF1
(
    REQ := xStart_String_to_Buf,       
    BUF_CNT := TO_DINT(sizeof_search_variable), 
    DONE => xDone_String_to_Buf, 
    SRC := input_search_variable, 
    BUFFER := output_search_variable
);

The first IF statement prevents the request from executing again. The request will be executed when the IEC port variable start_download_output_IEC becomes true and thus the variable start_download_input in the C++ code becomes true. This variable is set to true through user input. When the request is done, the port variable start_download_output in the C++ code will become true (see step 3 - Add input/output ports) and start_download_input_cpp becomes true. This will execute the IF statement and make the variable start_download_output_IEC false, stopping the request from executing again.

The second IF statement takes the user input and uses that input to start the conversion from string to buffer. The search variable entered by the user must be converted to a buffer only after the user is done. To do this, the string to buf conversion will only start when the variable xStart_String_to_Buf is true. If the button is pressed by the user, the variable search_button will become true and the IF statement will execute, making the variable xStart_String_to_Buf true which starts the conversion.

The third IF statement is used to solve the problem that the HTTP request can only be executed when the conversion is done, else the request could possibly be executed before the conversion is done. The variable xDone_String_to_Buf will become true as soon as the conversion is done, the IF statement will be executed and the variable start_download_output_IEC in turn will become true. This makes the variable start_download_input in the C++ code true and the request will execute. The variable xStart_String_to_Buf is made false to stop the conversion from starting over.

After the IF statements comes a line of code with the function LEN(), this function passes the length of the search variable string and gives this value to the variable sizeof_search_variable. This information is needed to make the conversion from string to buffer work.

As last we have the two conversion functions, one for string to buffer and one for buffer to string. We use STRING_TO_BUF to convert the search variable of the type string to a buffer and we use BUF_TO_STRING to convert the returned results from the buffer of the C++ code back to a string.
The function takes multiple variables as input and gives one variable as output. As input it expects the lenght of the string, the required signal to start the conversion, the source string or buffer to be converted and the string or buffer to which the converted output must go. As a output it gives one variable to indicate if the conversion is done or not.

The next thing we need to do is add all the variables we are going to use. Ignore all errors, save the code and go to the tab Variables. Enter all variables and make sure the type is set correctly, if you cannot find a type by scrolling, try to enter it manually and it should show up. After you are done, save the project and check the IEC code to make sure no errors exist.

list_of_IEC_variables

The variables can have different usages. When a variable is local, you can only use it in the IEC code itself and nowhere else. When a variable is external, the variable is locally usable but also able to be linked to a EHMI webserver. When a variable is defined as in- or output, the variable is locally usable but can also be connected as a port to in- and outputs from the C++ code. This last usage, variables as a port, is what I am going to show you next.

Link the input/output ports

To pass variables from one program to another we use ports. These ports need to be connected to eachother in the Port List of PLCnext Engineer. On the left hand side under Plant double-click on the tab PLCnext, this opens the Tasks and Events in the main window. Navigate to the tab Port List to find all connectable ports of your IEC and C++ program.

port_list

Scroll down till you see your variables/ports listed, the ports of your C++ program are denoted with MyComponent1 and the ports of the IEC program are denoted with Arp.Plc.Eclr/MainInstance.

Here, the ports need te be connected as shown below. To get an easy overview of which ports are connected and which are not, click on the filter on the left of the search bar and select the wanted filter. As a quick overview of which variables are linked to eachother I also made a table with all ports sorted

connected_variables_table

ports_connected

Make a EHMI webserver

We are almost done with all the functionality. The last thing that we need to do is make a GUI to be able to enter the search variable, to start the HTTP request and to show the returned results. We are going to use the EHMI webserver of PLCnext Engineer, this enables us to easily make a GUI which is accessed through a webserver

The first thing we need to do is add special tags to our variables. These tags are called HMI Tags and are used to signal that the variable is open for use in the EHMI webserver. Navigate to your controller, click on the tab axc-f-2152-1 : AXC F 2152 and in the main window navigate to the Data List. This will show you all variables you’ve created and specified as External for usage.

plant_controller_tab

data_list_external_variables

Right click on a variable and choose Add HMI Tag from the pop-up window. Do this for all three variables, the name of the tag can be found in the table at the column HMI Tag. You can also add the tag by clicking on the HMI logo at the top.

workbar_hmi_tag

That is all for the variables. The next thing we are going to do is make the webserver and link the variables. Expand the tab HMI Webserver on the left-hand side, right-click on Application and choose Add HMI Page. This will create two pages, one you created and one PLCnext Engineer created as a login page. You can not delete this page, as it will pop back up just as fast as you can delete it.

create_pages

This created login page is used to access the webserver, but can be turned off to make access to the webserver easier and faster. To do this, double-click on the tab HMI Webserver, all the settings will open including the setting Enforcement of user levels under the tab Security. Set the option to none to turn off the login, you can now delete the login page permanently. If you want to use the login page again, just turn on the security and the page should be created immediately.

You can name the created page as you like, I leave mine the way it is. Set the page as your homepage by right-clicking on the page and selecting Set HMI Page as Startup. After that, double-click on the page to open the editor in the main window.

HMI_page_editor

Here, you can design the webpage to your liking. You can change the background, add images, make multiple pages through which you can navigate and much more. Take a look at this HMI tutorial to get an idea of the possibilities.

On the right side of the editor in the components pane, expand the tab HMI. This gives you all the usable objects, images, symbols and templates for your wegpage. Expand the tab objects and drag the object Text Input into the main window.

HMI_tab_TextInput

Now do the same for the objects Text and Button, these are all the objects we are going to need. place the objects in a logical fashion, the text input and button grouped toghether and the text below it. Make the textbox for the text object a bit bigger to make sure the results fit in the textbox. To allign objects with eachother or to change their position using coördinates you can use the options at the top of the editor.

HMI_page_with_objects

To be able to use these objects for our IEC code we have to link the variables from the code to the objects. Objects have two tabs for the options, one tab is named after the object which is for the appearence and the static behaviour and one tab is is named Dynamics which is for the dynamic behaviour. With dynamic behaviour I mean a behaviour that is dependend on the state of other variables, objects or inputs. Select the text input object and go to this tab, click on the New dynamic option and choose Text.

new_dynamic

This will add the behaviour to the list and lets us choose a variable to link to it. Select the variable input_search_variable from the drop-down menu. Do the same for the text object but this time select the Result_string200 variable.

The button works a little different. Because it expects a user input the button takes a variable as normal behaviour, not as dynamic behaviour. Select the button object and expand the Behaviour tab, choose search_button as the variable and set the Value when pressed to true.

button_behaviour

After you’ve done this, write the project to your controller and start it up by using the cockpit in PLCnext Engineer.

Step 5 - Test the functionality

Congratulations! You are finished setting up and configuring the entire project and its functionality. It is now time to test it and possibly debug it if something is wrong or not working.

Test method

Navigate to the cockpit by double-clicking the tab axc-f-2152-1 : AXC F 2152 and opening the cockpit at the top of the main window. Here, you can access the HMI webbrowser by clicking on the page icon.

access_HMI_webserver

This will open up the webserver in your default browser. You can also access the HMI by writing the IP address of the controller in your browser and appending /ehmi/hmiapp.html to it. For my controller this would be 192.168.6.49/ehmi/hmiapp.html.

Next, type in your search variable in the textbox and click on the button. Enter a variable which you know to be existent in the database to make sure the test returns results. The HTTP request will be executed. If everything went according to plan the results should be showed on screen with the personID and the personName.

returned_results

Please have some patience as it can take a little while before the results are shown in the textbox. If you think it takes too long, press the button again.

Debug methods

If you think something is going wrong, check the values of the variables to make sure all change accordingly. To do this, expand the tab PLCnext in the Plant pane and expand the ESM1 and Cyclic100 tabs after that. This shows the programs running under Cyclic100, double-click on the program MainInstance : Main and the main window will open up.

MainInstance_tab

In this main window we can look at the variables with three differen methods, all methods are real-time. The first method is by looking in the Port List tab, here you can see all used ports and their respective values. This method enables you to see which values are passed to the C++ program, but it does not show the local variables.

debugging_port_list

The second method is by looking at the Variables list. This will show you all the variables of the IEC program, local and external, but not the variables of the C++ program.

debugging_variables_list

The third method is by looking at the code in the code tab, this shows you the code with the value of the variables written behind them.

debugging_code

We can also check the request in Azure. Go to your Logic App and open the overview. The overview shows the ammount of times the workflow has been executed and if this execution succeeded or failed.

logic_app_result_overview

Click on one of these executions and the workflow will open, not to alter the design but to view the data that has been passed during the execution. You can also see green check marks which indicate if the action was executed correctly, this way you can easily see at what action the execution went wrong.

run_history_individual_overview

If you expand one of these actions by clicking on it you can see the in- and outputs that the action used. This can come in handy to see how the headers were used in the workflow and which search variable was used for the stored procedure.

run_history_individual

That was all for this tutorial. I want to thank you for taking the effort to read it and I hope you found it to be helpful. All files I used can be found on my github page, feel free to download them and use them as you wish!


Appendix – Used codes

C++ program - MyComponent.cpp

#include "MyComponent.hpp"
#include "Arp/Plc/Commons/Esm/ProgramComponentBase.hpp"
#include <string> 
#include <string.h>

namespace Azure_connection_cpp
{
    void MyComponent::Initialize()
    {
        // never remove next line
        ProgramComponentBase::Initialize();
        // subscribe events from the event system (Nm) here
    }

    void MyComponent::LoadConfig()
    {
        // load project config here
    }

    void MyComponent::SetupConfig()
    {
        // never remove next line
        ProgramComponentBase::SetupConfig();
        // setup project config here
    }

    void MyComponent::ResetConfig()
    {
        // never remove next line
        ProgramComponentBase::ResetConfig();
        // implement this inverse to SetupConfig() and LoadConfig()
    }

    void MyComponent::Start(void) 
    {
        workerThreadInstance.Start();
        Log::Info("-------------------------------Thread started");
    }

    void MyComponent::Stop(void) 
    {
        workerThreadInstance.Stop();
        Log::Info("-------------------------------Thread stopped");
}

    /// Thread Body
    void MyComponent::workerThreadBody(void) 
    {   
        const char* input_search_variable = reinterpret_cast<char*>(search_var_byte);
        const char* Search_syntax = "search_variable: ";
        char variable_buffer[256];

        strncpy(variable_buffer, Search_syntax, sizeof(variable_buffer));
        strncat(variable_buffer, input_search_variable, sizeof(variable_buffer));

        if (start_download_input == true)
        {
            Log::Info("-------------------------------Thread HTTPS REQUEST: REQUEST STARTED");
            struct curl_slist* headers = NULL;
            CURL* curl;
            CURLcode res;

            const char* url = "https://prod-54.westeurope.logic.azure.com:443/workflows/a3138f986cb3499dabc5d1ecc42a8c38/triggers/manual/paths/invoke?api-version=2016-10-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=01stU3opM0XlI2kU55l6jxVqhQy1rxIgi1HErSEIFo4";
            const char* Content_Type = "Content-Type: application/json";
            const char* Content_Length_H = "Content-Length: 0";
            const char* Search_pointer = "search_pointer: @name";
            const std::string HTTP_buffer;
            headers = NULL;

            curl = curl_easy_init();
            if (curl)
            {
                curl_easy_setopt(curl, CURLOPT_URL, url);
                curl_easy_setopt(curl, CURLOPT_POST, 1L);

                headers = curl_slist_append(headers, Content_Type);
                headers = curl_slist_append(headers, Content_Length_H);
                headers = curl_slist_append(headers, Search_pointer);
                headers = curl_slist_append(headers, variable_buffer);

                curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
                curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_data);
                curl_easy_setopt(curl, CURLOPT_WRITEDATA, &HTTP_buffer);

                res = curl_easy_perform(curl);
                curl_easy_cleanup(curl);
                curl_slist_free_all(headers);
                Log::Info("-------------------------------Thread HTTPS REQEUST: RESPONSE RECEIVED");
            } 

            sizeof_result_string = HTTP_buffer.size(); 
            for (int i = 0; i <= sizeof_result_string; i++)
            {
                result_string_byte[i] = HTTP_buffer[i]; 
            }

            start_buffer = true;
            start_download_output = true;       
        }

        if(start_download_input == false)
        {
            start_buffer = false;
            start_download_output = false;            
        }
    }

    size_t MyComponent::write_data(void* ptr, size_t size, size_t nmemb, void* userp)
    {
        ((std::string*)userp)->append((char*)ptr, size * nmemb);
        return size * nmemb;
    }
} // end of namespace Azure_connection_cpp

C++ program – MyComponent.hpp

#pragma once
#include "Arp/System/Core/Arp.h"
#include "Arp/System/Acf/ComponentBase.hpp"
#include "Arp/System/Acf/IApplication.hpp"
#include "Arp/Plc/Commons/Esm/ProgramComponentBase.hpp"
#include "MyComponentProgramProvider.hpp"
#include "Azure_connection_cppLibrary.hpp"
#include "Arp/Plc/Commons/Meta/MetaLibraryBase.hpp"
#include "Arp/System/Commons/Logging.h"

//ADDED
#include "Arp/System/Acf/IControllerComponent.hpp"
#include "Arp/System/Commons/Threading/WorkerThread.hpp"
#include "Arp/System/Commons/Threading/Thread.hpp"
#include "Arp/System/Commons/Threading/ThreadSettings.hpp"
#include "curl.h" 
#include <string> 

namespace Azure_connection_cpp
{
    using namespace Arp;
    using namespace Arp::System::Acf;
    using namespace Arp::Plc::Commons::Esm;
    using namespace Arp::Plc::Commons::Meta;

    //#component
    class MyComponent : public ComponentBase
                    , public ProgramComponentBase
                    , public IControllerComponent
                    , private Loggable<MyComponent>
    {
    public: // typedefs

    public: // construction/destruction
        MyComponent(IApplication& application, const String& name);
        virtual ~MyComponent() = default;

    public: // IComponent operations
        void Initialize() override;
        void LoadConfig() override;
        void SetupConfig() override;
        void ResetConfig() override;

    public: // IControllerComponent operations
        void Start(void);
        void Stop(void);

    public: // ProgramComponentBase operations
        void RegisterComponentPorts() override;

    private: // methods
        MyComponent(const MyComponent& arg) = delete;
        MyComponent& operator= (const MyComponent& arg) = delete;

    public: // static factory operations
        static IComponent::Ptr Create(Arp::System::Acf::IApplication& application, const String& name);
        static size_t write_data(void* ptr, size_t size, size_t nmemb, void* userp);

    // Added: IProgramComponent operations
    public:
        IProgramProvider & GetProgramProvider(bool useBackgroundDomain) override;

    // Added: IMetaComponent operations
    public:
        IDataInfoProvider & GetDataInfoProvider(bool isChanging) override;
        IDataNavigator*     GetDataNavigator() override;

    private: // fields
        MyComponentProgramProvider programProvider;
        DataInfoProvider           dataInfoProvider;

        // Worker Thread Example
        WorkerThread workerThreadInstance;
        bool startflag = false; 

        void workerThreadBody(void);

    public: // Ports

            //#port
            //#attributes(Input)
            //#name(start_download_input)
            bool start_download_input = false;

            //#port
            //#attributes(Output)
            //#name(start_download_output)
            bool start_download_output = false;

            //#port
            //#attributes(Output)
            //#name(start_buffer)
            bool start_buffer = false;

            //#port
            //#attributes(Output)
            //#name(sizeof_result_string)
            int16 sizeof_result_string = 0;

            //#port
            //#attributes(Output)
            //#name(result_string_byte)
            uint8 result_string_byte[200];

            //#port
            //#attributes(Input)
            //#name(search_var_byte)
            uint8 search_var_byte[200];
    };

    ///////////////////////////////////////////////////////////////////////////////
    // inline methods of class MyComponent
    inline MyComponent::MyComponent(IApplication& application, const String& name)
    : ComponentBase(application, ::Azure_connection_cpp::Azure_connection_cppLibrary::GetInstance(), name, ComponentCategory::Custom)
    , programProvider(*this)
    , ProgramComponentBase(::Azure_connection_cpp::Azure_connection_cppLibrary::GetInstance().GetNamespace(), programProvider)

    // Added: data info provider
    , dataInfoProvider(::Azure_connection_cpp::Azure_connection_cppLibrary::GetInstance().GetNamespace(), &(this->programProvider))

    // Worker Thread Example
    , workerThreadInstance(make_delegate(this, &MyComponent::workerThreadBody) , 5000, "WorkerThreadName") 
    {
    }

    #pragma region IProgramComponent implementation
    inline IProgramProvider& MyComponent::GetProgramProvider(bool /*useBackgroundDomain*/)
    {
        return this->programProvider;
    }
    #pragma endregion

    #pragma region IMetaComponent implementation
    inline IDataInfoProvider& MyComponent::GetDataInfoProvider(bool /*useBackgroundDomain*/)
    {
        return this->dataInfoProvider;
    }

    inline IDataNavigator* MyComponent::GetDataNavigator()
    {
        return nullptr;
    }
    #pragma endregion

    inline IComponent::Ptr MyComponent::Create(Arp::System::Acf::IApplication& application, const String& name)
    {
        return IComponent::Ptr(new MyComponent(application, name));
    }
} // end of namespace Azure_connection_cpp

CMake – CMakeLists.txt

cmake_minimum_required(VERSION 3.13)

project(Azure_connection_cpp)

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 src/${WILDCARD_HEADER} intermediate/code/${WILDCARD_HEADER})
file(GLOB_RECURSE Sources src/${WILDCARD_SOURCE} intermediate/code/${WILDCARD_SOURCE})
add_library(Azure_connection_cpp SHARED ${Headers} ${Sources})

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

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

target_include_directories(Azure_connection_cpp
    PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/intermediate/code>
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>)

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

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

list(INSERT CMAKE_MODULE_PATH 0 "${ARP_TOOLCHAIN_CMAKE_MODULE_PATH}")
list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake")

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

################# 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(Azure_connection_cpp PRIVATE LINKER:--no-undefined)

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

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

find_package(ArpDevice REQUIRED)
find_package(ArpProgramming REQUIRED)
find_package(CURL REQUIRED)

target_link_libraries(Azure_connection_cpp PRIVATE ArpDevice ArpProgramming curl::curl)

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

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

string(REGEX REPLACE "^.*\\(([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+).*$" "\\1" _ARP_SHORT_DEVICE_VERSION ${ARP_DEVICE_VERSION})
install(TARGETS Azure_connection_cpp
    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)

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

CMake – FindCURL.cmake

set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE BOTH)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY BOTH)

find_path(CURL_INCLUDE_DIR
    NAMES curl.h
    PATHS external/curl_build_ARM/include/curl
)
find_library(CURL_LIBRARY
    NAMES curl
    PATHS external/curl_build_ARM/lib
)

include(FindPackageHandleStandardArgs)

find_package_handle_standard_args(curl
    DEFAULT_MSG
    CURL_INCLUDE_DIR CURL_LIBRARY
)

if(CURL_FOUND)
    set(CURL_LIBRARIES ${CURL_LIBRARY})
    set(CURL_INCLUDE_DIRS ${CURL_INCLUDE_DIR})
endif()

if(CURL_FOUND AND NOT TARGET curl::curl)
    add_library(curl::curl UNKNOWN IMPORTED)
    set_target_properties(curl::curl PROPERTIES
        IMPORTED_LOCATION "${CURL_LIBRARY}"
        INTERFACE_INCLUDE_DIRECTORIES "${CURL_INCLUDE_DIR}"
    )
endif()

mark_as_advanced(
    CURL_INCLUDE_DIR CURL_INCLUDE_DIRS
    CURL_LIBRARY CURL_LIBRARIES)

IEC program – Main

IF (start_download_input_cpp = TRUE) THEN
    start_download_output_IEC := FALSE;
END_IF

IF (search_button = TRUE) THEN
    xStart_String_to_Buf := TRUE;
END_IF

IF (xDone_String_to_Buf = TRUE) THEN
    xStart_String_to_Buf := FALSE;
    start_download_output_IEC := TRUE;
END_IF

sizeof_search_variable := TO_UDINT(LEN(input_search_variable));

BUF_TO_STRING1
(
    REQ := xStart_Buf_to_String, 
    BUF_CNT := TO_DINT(sizeof_result_string_IEC), 
    DONE => xDone_Buf_to_String, 
    BUFFER := result_string_byte_IEC, 
    DST := Result_string200
);   

STRING_TO_BUF1
(
    REQ := xStart_String_to_Buf,       
    BUF_CNT := TO_DINT(sizeof_search_variable), 
    DONE => xDone_String_to_Buf, 
    SRC := input_search_variable, 
    BUFFER := output_search_variable
);

Comments  

# mboers 2020-01-11 17:20
Excellent description of a very interesting application.
# damianbombeeck 2020-01-11 22:53
Thank you!