This stage explains how to properly install a library to use it on STM32MP1 boards.
1. Overview[edit source]
Refer to the OPC UA overview for more details about this communication protocol.
2. Installation process[edit source]
This section gives an overview of how to get the open62541 library for STM32MP1.
2.1. Install the SDK[edit source]
- If the SDK is not yet installed, refer to STM32MP1_Developer_Package#Installing_the_SDK chapter before going further. It is essential later to cross-compile the library.
2.2. Create a new project[edit source]
It is not required to follow this step, however this tutorial is based on a fake project to give an example of how to proceed.
- Create a directory to host the source codes
mkdir $HOME/Documents/OPC_UA_first_project mkdir $HOME/Documents/OPC_UA_first_project/src
- Create a directory to host the library and go to it
mkdir $HOME/Documents/OPC_UA_first_project/lib cd $HOME/Documents/OPC_UA_first_project/lib
2.3. Clone open62541 git repository[edit source]
- Clone the repository needed to compile the library (git commands must be enable on your Linux):
git clone https://github.com/open62541/open62541.git cd open62541 git submodule update --init --recursive mkdir build && cd build
2.4. Cross-building the library[edit source]
The SDK is useful as it builds a library usable with ARM binaries to execute the code on the cortex A7 of STM32MP1.
- Source the shell to cross compile with the SDK:
source <path_to_SDK>/environment-setup-cortexa7t2hf-neon-vfpv4-ostl-linux-gnueabi
- To make sure that your source succeeds try "echo $CROSS_COMPILE" to get the following answer:
echo $CROSS_COMPILE arm-ostl-linux-gnueabi-
- To cross compile the library use the cmake command. All compilation flags can be found here on the official documentation of open62541. The main ones have been chosen in this example to make both Client/Server and PubSub communication.
cmake -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=MinSizeRel -DUA_ENABLE_PUBSUB=ON -DUA_ENABLE_PUBSUB_ETH_UADP=ON -DUA_NAMESPACE_ZERO=MINIMAL UA_ENABLE_STATUSCODE_DESCRIPTIONS=OFF UA_ENABLE_TYPEDESCRIPTION=OFF UA_LOGLEVEL=200 ..
make
- The library is now built and put into the bin folder. Check it here:
ls bin/ libopen62541.a
- To be sure that the library is well cross-compiled, enter the following command:
readelf -a bin/libopen62541.a
Many R_ARM notices are displayed with at the end, the following returns:
File Attributes Tag_CPU_name: "Cortex-A7" Tag_CPU_arch: v7 Tag_CPU_arch_profile: Application
3. Building your first program[edit source]
As similar to other libraries, the Makefile must be edited with the path information where to find open62541 library, headers etc. As a first program the example used here is given by open62541 team, which is a simple OPC UA Client .
- First, go to your src project folder
cd $HOME/Documents/OPC_UA_first_project/src
- Create a new file called myServer.c whose content can be found here. Do not compile the code as it is done on the webpage.
#include <open62541/plugin/log_stdout.h>
#include <open62541/server.h>
#include <open62541/server_config_default.h>
#include <signal.h>
#include <stdlib.h>
static volatile UA_Boolean running = true;
static void stopHandler(int sig) {
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "received ctrl-c");
running = false;
}
int main(void) {
signal(SIGINT, stopHandler);
signal(SIGTERM, stopHandler);
UA_Server *server = UA_Server_new();
UA_ServerConfig_setDefault(UA_Server_getConfig(server));
UA_StatusCode retval = UA_Server_run(server, &running);
UA_Server_delete(server);
return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}
- Now create a new file called Makefile whose content is:
# Makefile template for OPC UA
PROG = myServer.bin
SRCS = myServer.c #you can add here your other .c files
OBJS = $(SRCS:%.c=%.o)
#the path of open62541 library
PATHLIB = -L ../lib/open62541/build/bin/
#the name of the library
LIB = open62541
#path of headers files
INCLUDE += -I ../lib/open62541/include/
INCLUDE += -I ../lib/open62541/plugins/include/
INCLUDE += -I ../lib/open62541/build/src_generated/
INCLUDE += -I ../lib/open62541/arch/
INCLUDE += -I ../lib/open62541/deps/
INCLUDE += -I ../lib/open62541/src/pubsub/
CLEANFILES = $(PROG)
# Add / change option in CFLAGS and LDFLAGS
CFLAGS += -Wall
CFLAGS += $(INCLUDE)
LDFLAGS += $(PATHLIB) -l$(LIB)
all: $(PROG)
$(PROG): $(OBJS)
$(CC) -o $@ $^ $(LDFLAGS)
clean:
rm -f $(CLEANFILES) $(patsubst %.c,%.o, $(SRCS))
- Now build the program:
make
- The executable program appears in the folder:
ls Makefile myServer.bin myServer.c myServer.o
4. Executing the program on STM32MP1[edit source]
- Put myServer.bin on the board. This may be done in different ways, refer to How to cross-compile with the Developer Package part 9.3 for more information. In this case there is a simple way to do this:
scp myServer.bin root@<IP of your board>:/usr/local
- Then open another shell and connect to the board in ssh:
ssh root@<IP of your board>
- Go to the folder where the program is located and execute it:
cd /usr/local ./myServer.bin
- Output similar appears:
[2021-01-14 09:41:22.659 (UTC+0100)] warn/server AccessControl: Unconfigured AccessControl. Users have all permissions. [2021-01-14 09:41:22.659 (UTC+0100)] info/server AccessControl: Anonymous login is enabled [2021-01-14 09:41:22.659 (UTC+0100)] warn/server Username/Password configured, but no encrypting SecurityPolicy. This can leak credentials on the network. [2021-01-14 09:41:22.659 (UTC+0100)] warn/userland AcceptAll Certificate Verification. Any remote certificate will be accepted. [2021-01-14 09:41:22.659 (UTC+0100)] info/network TCP network layer listening on <TCP port>
The first OPC UA program is now running on your board
5. Going further: make your first OPC UA Client/Server[edit source]
As an example to illustrate this article, let's make an OPC UA Client/Server run on two different STM32MP1 (it can also be done with two shells on the PC, or two different PCs, or one PC and one board. Just make sure to have the right version of the library, cross-compiled or not, to do this). The two STM32MP1 are linked by Ethernet to the same network. The PC used for the development of the project is also in the same network (this helps to easily send the files by scp command later).
- Since two boards are used in this example, in order to not forget source the shell:
source <path_to_SDK>/environment-setup-cortexa7t2hf-neon-vfpv4-ostl-linux-gnueabi
- Then create all the folders needed for this little project, with the library side:
mkdir <your_project_path>/opc_ua_client_server && cd <your_project_path>/opc_ua_client_server mkdir lib && cd lib git clone https://github.com/open62541/open62541.git cd open62541 git submodule update --init --recursive mkdir build && cd build cmake -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=MinSizeRel -DUA_ENABLE_PUBSUB=ON -DUA_ENABLE_PUBSUB_ETH_UADP=ON -DUA_NAMESPACE_ZERO=MINIMAL UA_ENABLE_STATUSCODE_DESCRIPTIONS=OFF UA_ENABLE_TYPEDESCRIPTION=OFF UA_LOGLEVEL=200 .. make
- Now, with the source code side add:
cd ../../.. mkdir src && cd src
- Create a common file for both client and server common.h:
#ifndef COMMON_H
#define COMMON_H
const char * IP_SERVER = "10.48.0.23"; //the address needs to be the IP of your board which is your server (ifconfig)
const int PORT_SERVER = 12345; //choose your port for the server
#endif /* COMMON_H */
- Now create files for the server:
mkdir server && cd server
- Create myServer.c and Makefile
/** DATA MODEL INFORMATION **
*
*
*
* ------* Supermarket
* |
* |
* |--------* Products
* | |
* | |
* | |----------* Apples (R/W)
* | |
* | |
* | |----------* Bananas (R/W)
* |
* |
* |--------* Name (R)
*
*
* The information model is based on a nodes model, and each endpoint of the tree has different values with Read/Write rights (R/W).
* The client will be able to manage the quantity of fruits, but only read the name of the market.
*
*/
#include <open62541/plugin/log_stdout.h>
#include <open62541/server.h>
#include <open62541/server_config_default.h>
#include <signal.h>
#include <stdlib.h>
#include "../common.h"
static UA_VariableAttributes vnAttr;
static UA_VariableAttributes tpAttr;
static UA_VariableAttributes bAttr;
/* end tree node */
static UA_NodeId nodeApples;
static UA_NodeId nodeBananas;
static UA_NodeId nodeName;
static volatile UA_Boolean running = true;
static void stopHandler(int sig) {
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "received ctrl-c");
running = false;
}
//display the data model tree
static void display_tree();
// Read callback
static void
readCallback(UA_Server *server,
const UA_NodeId *sessionId, void *sessionContext,
const UA_NodeId *nodeid, void *nodeContext,
const UA_NumericRange *range, const UA_DataValue *data) {
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "read from client on node : %.*s ",
(int)nodeid->identifier.string.length, nodeid->identifier.string.data);
}
// Write callback
static void
writeCallback(UA_Server *server,
const UA_NodeId *sessionId, void *sessionContext,
const UA_NodeId *nodeid, void *nodeContext,
const UA_NumericRange *range, const UA_DataValue *data) {
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "write from client on node : %.*s", (int)nodeid->identifier.string.length, nodeid->identifier.string.data);
}
int main(int argc, char * argv[]) {
signal(SIGINT, stopHandler);
signal(SIGTERM, stopHandler);
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Initialisation ...\n");
UA_Server * server = UA_Server_new();
//get port number
UA_Int16 port_number = (UA_Int16) PORT_SERVER;
//Server config creation
UA_ServerConfig_setMinimal(UA_Server_getConfig(server), port_number, 0);
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Configuration completed\n");
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Server created\n");
//--------------------------------------------DATA-------------------------------------------
//Adding a new namepace to the server
UA_Int16 ns_supermarket = UA_Server_addNamespace(server,"NS_Supermarket");
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "New Namespace added with the number : %d",ns_supermarket);
//adding a new Object Supermarket which is the major Node
UA_NodeId node_supermaret_id; /* get the nodeid assigned by the server */
UA_ObjectAttributes sAttr = UA_ObjectAttributes_default;
UA_Server_addObjectNode(server, UA_NODEID_STRING(2,"Node_Supermarket"),
UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
UA_QUALIFIEDNAME(2, "Supermarket"), UA_NODEID_NUMERIC(0, UA_NS0ID_BASEOBJECTTYPE),
sAttr, NULL, &node_supermaret_id);
//adding the variable Name to server
vnAttr = UA_VariableAttributes_default;
UA_String marketName = UA_STRING("Fruits_Market_And_Co.");
vnAttr.accessLevel = UA_ACCESSLEVELMASK_READ;
UA_Variant_setScalar(&vnAttr.value, &marketName, &UA_TYPES[UA_TYPES_STRING]);
UA_Server_addVariableNode(server, UA_NODEID_STRING(2,"Node_Market_Name"), node_supermaret_id,
UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
UA_QUALIFIEDNAME(2, "Market_Name"),
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), vnAttr, NULL, &nodeName);
//adding a new Object Product which is a child Node of Supermarket
UA_NodeId node_products_id; /* get the nodeid assigned by the server */
UA_ObjectAttributes oAttr = UA_ObjectAttributes_default;
UA_Server_addObjectNode(server, UA_NODEID_STRING(2,"Node_Products"),
node_supermaret_id,
UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
UA_QUALIFIEDNAME(2, "Products"), UA_NODEID_NUMERIC(0, UA_NS0ID_BASEOBJECTTYPE),
oAttr, NULL, &node_products_id);
//adding the variable node Apples under products
tpAttr = UA_VariableAttributes_default;
UA_UInt32 apples = 60;
tpAttr.accessLevel = UA_ACCESSLEVELMASK_READ ^ UA_ACCESSLEVELMASK_WRITE;
UA_Variant_setScalar(&tpAttr.value, &apples, &UA_TYPES[UA_TYPES_UINT32]);
UA_Server_addVariableNode(server, UA_NODEID_STRING(2,"Node_Apples"), node_products_id,
UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
UA_QUALIFIEDNAME(2, "Apples"),
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), tpAttr, NULL, &nodeApples);
//adding the variable node Bananas under products
bAttr = UA_VariableAttributes_default;
UA_UInt32 bananas = 34;
bAttr.accessLevel = UA_ACCESSLEVELMASK_READ ^ UA_ACCESSLEVELMASK_WRITE;
UA_Variant_setScalar(&bAttr.value, &bananas, &UA_TYPES[UA_TYPES_UINT32]);
UA_Server_addVariableNode(server, UA_NODEID_STRING(2,"Node_Bananas"), node_products_id,
UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
UA_QUALIFIEDNAME(2, "Bananas"),
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), bAttr, NULL, &nodeBananas);
//Adding Callback to all Nodes
UA_ValueCallback callback ;
callback.onRead = readCallback;
callback.onWrite = writeCallback;
UA_Server_setVariableNode_valueCallback(server, UA_NODEID_STRING(2,"Node_Bananas"), callback);
UA_Server_setVariableNode_valueCallback(server, UA_NODEID_STRING(2,"Node_Apples"), callback);
UA_Server_setVariableNode_valueCallback(server, UA_NODEID_STRING(2,"Node_Market_Name"), callback);
//-------------------------------------------------------------------------------------------
display_tree(server);
UA_StatusCode retval = UA_Server_run(server, &running);
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "shutdown server ...");
display_tree(server);
UA_Server_delete(server);
return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}
//display the data model tree information (server side)
static void display_tree(UA_Server * serv) {
UA_String str = * (UA_String *) vnAttr.value.data;
UA_Variant value;
UA_Variant_init(&value);
UA_StatusCode retval = UA_Server_readValue(serv, nodeApples ,&value);
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, ">>> %d", retval);
UA_UInt32 nA = * (UA_UInt32 *) value.data;
UA_Server_readValue(serv, nodeBananas ,&value);
UA_UInt32 nB = * (UA_UInt32 *) value.data;
printf("\n\n");
printf("------* Supermarket\n");
printf(" |\n");
printf(" |\n");
printf(" |--------* Products\n");
printf(" | |\n");
printf(" | |\n");
printf(" | |----------* Apples n = %d\n", (int) nA);
printf(" | |\n");
printf(" | |\n");
printf(" | |----------* Bananas n = %d\n", (int) nB);
printf(" |\n");
printf(" |\n");
printf(" |--------* Name = %.*s", (int)str.length, str.data);
printf("\n\n");
UA_Variant_clear(&value);
}
# Makefile template for OPC UA
PROG = myServer.bin
SRCS = myServer.c #you can add here your other .c files
OBJS = $(SRCS:%.c=%.o)
#the path of open62541 library
PATHLIB = -L ../../lib/open62541/build/bin/
#the name of the library
LIB = open62541
#path of headers files
INCLUDE += -I ../../lib/open62541/include/
INCLUDE += -I ../../lib/open62541/plugins/include/
INCLUDE += -I ../../lib/open62541/build/src_generated/
INCLUDE += -I ../../lib/open62541/arch/
INCLUDE += -I ../../lib/open62541/deps/
CLEANFILES = $(PROG)
# Add / change option in CFLAGS and LDFLAGS
CFLAGS += -Wall
CFLAGS += $(INCLUDE)
LDFLAGS += $(PATHLIB) -l$(LIB)
all: $(PROG)
$(PROG): $(OBJS)
$(CC) -o $@ $^ $(LDFLAGS)
clean:
rm -f $(CLEANFILES) $(patsubst %.c,%.o, $(SRCS))
- Build the server part:
make
- The binary myServer.bin is now cross-compiled and can be sent on the server STM32MP1 board.
scp myServer.bin root@<IP of your server board>:/usr/local
- Now do the same with client side:
cd .. mkdir client && cd client
- Create myClient.c and Makefile:
#include <open62541/client_config_default.h>
#include <open62541/client_highlevel.h>
#include <open62541/plugin/log_stdout.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include "../common.h"
static volatile UA_Boolean running = true;
static void stopHandler(int sig) {
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "received ctrl-c");
running = false;
}
int main(void) {
signal(SIGINT, stopHandler);
signal(SIGTERM, stopHandler);
/* Create the client wich will listen on the port described in common.h */
UA_Client *client = UA_Client_new();
UA_ClientConfig_setDefault(UA_Client_getConfig(client));
char address[64];
char header[sizeof("opc.tcp://")] = "opc.tcp://";
strcat(address, header);
strcat(address, IP_SERVER);
strcat(address, ":");
char port[12];
sprintf(port, "%d", PORT_SERVER);
strcat(address, port);
UA_StatusCode retval = UA_Client_connect(client, address); //IP / port of your server
if(retval != UA_STATUSCODE_GOOD) {
UA_Client_delete(client);
return (int)retval;
}
//begin routine ---------------------
while (running == true){
//Variable to read from Server
UA_String marketName;
UA_UInt32 applesNumber;
UA_UInt32 bananasNumber;
//Variant that is used as buffer
UA_Variant value;
UA_Variant_init(&value);
//We read the name of the Supermarket
retval = UA_Client_readValueAttribute(client, UA_NODEID_STRING(2,"Node_Market_Name"), &value);
if(retval == UA_STATUSCODE_GOOD &&
UA_Variant_hasScalarType(&value, &UA_TYPES[UA_TYPES_STRING])) {
marketName = *(UA_String *) value.data;
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,"The Market Name is : %.*s \n",(int)marketName.length, marketName.data);
}
sleep(1);
//We try to modify the name of Supermarket (should not work because we do not have writing rights)
UA_String newName = UA_STRING("My_New_Fruit_Market.");
UA_Variant_setScalar(&value, &newName, &UA_TYPES[UA_TYPES_STRING]);
retval = UA_Client_writeValueAttribute(client, UA_NODEID_STRING(2,"Node_Market_Name"), &value);
if(retval != UA_STATUSCODE_GOOD){
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "You do not have rights to modify the market name\n");
}
sleep(1);
//We read the number of apples in the market
retval = UA_Client_readValueAttribute(client, UA_NODEID_STRING(2,"Node_Apples"), &value);
if(retval == UA_STATUSCODE_GOOD &&
UA_Variant_hasScalarType(&value, &UA_TYPES[UA_TYPES_UINT32])) {
applesNumber = *(UA_UInt32 *) value.data;
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,"The number of Apples is : %.d \n",(int)applesNumber);
}
sleep(1);
//We try to change the number of apples in the market (should work because we have writing rights)
UA_UInt32 valA = applesNumber + 2;
UA_Variant_setScalar(&value, &valA, &UA_TYPES[UA_TYPES_UINT32]);
retval = UA_Client_writeValueAttribute(client, UA_NODEID_STRING(2,"Node_Apples"), &value);
if(retval != UA_STATUSCODE_GOOD){
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Error when trying to change Apples number\n");
}
sleep(1);
//We read the number of bananas in the market
retval = UA_Client_readValueAttribute(client, UA_NODEID_STRING(2,"Node_Bananas"), &value);
if(retval == UA_STATUSCODE_GOOD &&
UA_Variant_hasScalarType(&value, &UA_TYPES[UA_TYPES_UINT32])) {
bananasNumber = *(UA_UInt32 *) value.data;
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,"The number of Bananas is : %.d \n",(int)bananasNumber);
}
sleep(1);
//We try to change the number of apples in the market (should work because we have writing rights)
UA_UInt32 valB = bananasNumber + 1;
UA_Variant_setScalar(&value, &valB, &UA_TYPES[UA_TYPES_UINT32]);
retval = UA_Client_writeValueAttribute(client, UA_NODEID_STRING(2,"Node_Bananas"), &value);
if(retval != UA_STATUSCODE_GOOD){
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Error when trying to change Bananas number\n");
}
sleep(1);
}
//-----------------------------------
/* Clean up */
UA_Client_delete(client); /* Disconnects the client */
return EXIT_SUCCESS;
}
# Makefile template for OPC UA
PROG = myClient.bin
SRCS = myClient.c #you can add here your other .c files
OBJS = $(SRCS:%.c=%.o)
#the path of open62541 library
PATHLIB = -L ../../lib/open62541/build/bin/
#the name of the library
LIB = open62541
#path of headers files
INCLUDE += -I ../../lib/open62541/include/
INCLUDE += -I ../../lib/open62541/plugins/include/
INCLUDE += -I ../../lib/open62541/build/src_generated/
INCLUDE += -I ../../lib/open62541/arch/
INCLUDE += -I ../../lib/open62541/deps/
CLEANFILES = $(PROG)
# Add / change option in CFLAGS and LDFLAGS
CFLAGS += -Wall
CFLAGS += $(INCLUDE)
LDFLAGS += $(PATHLIB) -l$(LIB)
all: $(PROG)
$(PROG): $(OBJS)
$(CC) -o $@ $^ $(LDFLAGS)
clean:
rm -f $(CLEANFILES) $(patsubst %.c,%.o, $(SRCS))
- Build the client part:
make
- The binary myClient.bin is now cross-compiled and it can sent on the client STM32MP1 board.
scp myClient.bin root@<IP of your client board>:/usr/local
Now, open a shell for both of your boards, and go to /usr/local/ folder, find binaries and execute them starting with the server.