Writing an OPC UA Application
Overview
An application for the OPC UA server is a GOAL application. It consists of three functions that are called by GOAL: appl_init, appl_setup, appl_loop.
Additionally, the application can register a callback that is called by the OPC UA Master stack to inform the application about events.
The function appl_init is used to register components in GOAL, e.g. the OPC UA server stack.
The actual initialization of the application happens in appl_setup.
The application must include goal_includes.h and goal_opcua.h.
Registration of the OPC UA Stack
Within the function appl_init the function goal_opcUaInit must be called.
GOAL_STATUS_T res; /* result */
res = goal_opcUaInit();
if (GOAL_RES_ERR(res)) {
goal_logErr("Initialization of OPC UA failed");
}
Configuration of the OPC UA Stack
The function appl_setup is called by GOAL during initialization. Within this function, all functions listed in this chapter can be used to configure the behavior of the OPC UA. All functions must be called before calling goal_opcUaNew.
All functions return a status code indicating whether the operation succeeded or not.
Function | Description |
---|---|
goal_opcUaCfgSecurityPolicyBasic256Sha256Enable | Enable support for the SecurityPolicyBasic256Sha256 |
goal_opcUaCfgSecurityPolicyNoneDisable | Disable support for SecurityPolicyNone |
goal_opcUaCfgSecurityModeSet | Set the supported SecurityModes |
goal_opcUaCfgUsernamePasswordTableAdd | Add the table with acceptable usernames and passwords |
goal_opcUaCfgMsgBufferSizeSet | Set the size of the reception and send buffers |
goal_opcUaCfgNumInputArgsSet | Set the maximum number of input arguments for methods |
goal_opcUaCfgNumOutputArgsSet | Set the maximum number of output arguments for methods |
Creating a new instance of the OPC UA protocol stack
After the stack has been configured, the function goal_opcUaNew must be invoked to create a new instance of the protocol stack. It is also used to register a callback handler for processing events from the stack. The callback handler will be explained in detail in a later chapter.
GOAL_STATUS_T res; /* result */
GOAL_OPCUA_T *pHdlOpcUa; /* GOAL OPC UA handle */
res = goal_opcUaNew(&pHdlOpcUa, GOAL_OPCUA_INSTANCE_DEFAULT, appl_opcUaCallbackExec);
if (GOAL_RES_ERR(res)) {
goal_logErr("failed to create a new OPC UA instance");
return res;
}
This function creates a handle (in this example it is called pHdlOpcUa) that must be used for all other function calls to reference the stack instance.
Application Callback
During initialization, the application can register a callback handler with the function goal_opcUaNew. The callback handler uses the following arguments:
Argument data type | Description |
---|---|
GOAL_OPCUA_T * | GOAL OPC UA instance |
GOAL_OPCUA_CB_ID_T | callback ID indicating callback type |
GOAL_OPCUA_CB_DATA_T * |
|
Some callback ID also evaluate the return value of the handler to decide how to proceed.
Callback ID | Description | Callback data | Return value |
---|---|---|---|
GOAL_OPCUA_CB_ID_NODE_CREATED | a node was created that is based on an application specific type | pData->data[0].pSessionNodeId | GOAL_OK: accept new node other: abort creation |
GOAL_OPCUA_CB_ID_NODE_DELETED | a node was deleted that is based on an application specific type | pData->data[0].pSessionNodeId | don’t care |
GOAL_OPCUA_CB_ID_VARIABLE_READ | the value of a variable node is going to be read | pData->data[0].pSessionNodeId | don’t care |
GOAL_OPCUA_CB_ID_VARIABLE_WRITTEN | the value of a variable node was written | pData->data[0].pSessionNodeId | don’t care |
The following listing shows how to handle a callback:
static GOAL_STATUS_T appl_opcUaCallbackExec(
GOAL_OPCUA_T *pOpcUa, /**< GOAL OPC UA instance */
GOAL_OPCUA_CB_ID_T cbId, /**< callback Id */
GOAL_OPCUA_CB_DATA_T *pData /**< callback data */
)
{
GOAL_STATUS_T res = GOAL_OK; /* result */
switch (cbId) {
/* ... */
case GOAL_OPCUA_CB_ID_VARIABLE_READ:
goal_logInfo("Read access to Node %s", goal_opcUaNodeIdPrint(pOpcUa, pData->data[1].pVariableNodeId));
if (GOAL_TRUE == goal_opcUaNodeIdCompare(pData->data[1].pVariableNodeId, &var01Node)) {
goal_logInfo("Variable 01 is going to be read");
/* increase value with each read */
var01++;
GOAL_htole32_p((uint8_t *) pData->data[3].pDataValue->value.pValue, var01);
}
break;
/* ... */
}
return res;
}
Helper functions
The functions listed in this chapter are provided for convenience.
Function | Description |
---|---|
goal_opcUaNodeIdCompare | check if two node IDs have the same value |
goal_opcUaNodeIdPrint | create a printable string of a node ID |
goal_opcUaByteStringPrint | create a printable string of a ByteString |
goal_opcUaNodeFindByName | find a node by reference and BrowseName |
goal_opcUaNodeIdNumericSet | create a numeric node ID |
goal_opcUaNodeIdStringSet | create a String node ID from a String |
goal_opcUaNodeIdStringCharSet | create a String node ID from a char array |
goal_opcUaNodeIdGuidSet | create a GUID node ID |
goal_opcUaNodeIdByteStringSet | create a ByteString node ID |
goal_opcUaStringSet | populate a String with a char array |
goal_opcUaQualifiedNameSet | populate a QualifiedName with a char array |
goal_opcUaLocalTextSet | populate a LocalicedText with two char arrays (locale and text) |
goal_opcUaVariantScalarSet | populate a Variant with a scalar value |
goal_opcUaVariantArraySet | populate a Variant with an array value |
goal_opcUaVariantArrayDimensionsSet | populate a Variant with an array value and its dimension sizes |
Fieldbus Variable Mapping
The GOAL OPC UA stack provides functions that allow the mapping of variables from a fieldbus to variable nodes.
Each fieldbus must register a callback handler first. The fieldbus is identified by an ID that must be unique for each fieldbus. For every fieldbus that is supported by GOAL there is already a GOAL_ID_* value. The callback handler is called whenever a variable node that is mapped to a fieldbus variable was written. Additionally, a callback argument can be set that will be passed to the callback handler.
For each variable that should be mapped to a variable node the function goal_opcUaFieldbusVarMap must be called. Each variable can be identified by up to three IDs. The meaning of these IDs is completely defined by the fieldbus protocol. For example a PROFINET variable can be identified by its Module ID and its Submodule ID and an index. An EtherCAT variable could be identified by an index and a subindex.
The data type of the variable must be specified. For non-numeric data types also the maximum size of the variable must be set. The node Id must reference a node that already exists.
Whenever a new value is received from the fieldbus the function goal_opcUaFbVarWrite must be invoked to update the value of the corresponding variable node.
As mentioned above, the registered callback handler is executed whenever a value was written to a mapped variable node.
AddressSpace node creation
The stack provides functions to create new nodes in the AddressSpace. Each function expects a node Id that will be used for the new node. Furthermore, other node IDs are passed to indicate the parent node of the new node, the reference type between the parent node and the new node, the type node (for object, variable or variableType nodes). Each node must have a BrowseName and attributes that are specific for each node class. If the parameter pCreatedNodeId is not NULL, the ID of the newly created node is written to this handle.
Function | Description |
---|---|
goal_opcUaNodeClassObjectAdd | create an object node |
goal_opcUaNodeClassObjectTypeAdd | create an objectType node |
goal_opcUaNodeClassVariableAdd | create a variable node |
goal_opcUaNodeClassVariableTypeAdd | create a variableType node |
goal_opcUaNodeClassViewAdd | create a view node |
goal_opcUaNodeClassReferenceTypeAdd | create a referenceType node |
goal_opcUaNodeClassDataTypeAdd | create a dataType node |
goal_opcUaNodeClassMethodAdd | create a method node |
goal_opcUaNodeReferenceAdd | create an additional reference between a source and a target node |
goal_opcUaNodePropertyAdd | add a property to a node |
The function goal_opcUaNodeClassMethodAdd require additional arguments. A callback handler that is triggered whenever the method is executed. Additionally, the number and properties of input and output arguments.
Event handling
EventTypes are objectTypes that are subtypes of the BaseEventType or its subtypes.
The function goal_opcUaEventCreate is used to create events of a specific eventType. The function also sets the properties severity (1: notification .. 1000: catastrophe), message and source name. The function creates a new node. Its ID is passed to pEventId.
The function goal_opcUaEventTrigger is used to trigger the event that is specified by its node ID.
Date Time base
This part only describes the GOAL OPC UA server stack. The open62541 stack uses the Linux time stamps to create current date time values. All API, which is presented here, will return a “Not Supported“ GOAL error code.
OPC UA messages contain a current data time value. Because not all platforms support a Real Time Clock (RTC), the data time must be emulated. Therefore, a data time value is stored at a specific time, and the tuple (date time, timestamp) is used as base to create date time values on given timestamps. Before a valid date time is extracted, the stack uses 0 values for date times, which are interpreted as “unset“ or “unused“. The tuple (date time, timestamp) can be created in two different ways.
Automatically: In this case, the stack tries to extract current date times from client messages. After the first successful extraction, the tuple (date time, timestamp) is used for all further calculations.
Manually: If an external time source is used (e.g. linux, a RTC or any other protocol), the stack can be informed directly using the function goal_opcUaDateTimeSet. Therefore, the current date time must be calculated, which is defined as the number of 100ns steps from 1601-01-01T00:00:00.000 (UTC) encoded as 64-bit integer. E.g. 1970-01-01T00:00:00.000Z corresponds to the date time value 0x019db1ded53e8000. This function could be called multiple times, overwriting the last tuple (date time, timestamp).