C++ Based OPC UA Client/Server/PubSub SDK  1.7.6.537
Server Address Space

The Unified Automation C++ Server SDK provides several options for creating the server address space.

Introduction

The OPC UA server address space contains nodes that provide information from the underlying system that is represented by the OPC UA server. A very simple address space structure can be created with folder objects. In such a simple structure, the folders contain data Variables representing the data of the system.

Most OPC UA servers use specific ObjectTypes for the objects they provide in their address space. These ObjectTypes are typically defined by standard information models or by the server vendor. ObjectTypes define member variables and optionally methods. More details can be found in the Address Space Concepts section.

The address space consists of nodes such as object, variable or method nodes. These nodes are connected by references. A node belongs to a Namespace.

In the C++ SDK, a namespace is implemented by a NodeManager. The class NodeManagerBase implements the NodeManager and the IOManager interfaces.

The namespaces for index 0 (OPC UA) and 1 (local server) are implemented and handled by OPC UA server SDKs. They are automatically created during server start-up. Additional NodeManagers are added by the application specific code in the server main function as shown in the following simplified sample code.

// Create and initialize server object
OpcServer* pServer = new OpcServer;
// Add NodeManager for application specific nodes
MyNodeManager *pMyNodeManager = new MyNodeManager();
pServer->addNodeManager(pMyNodeManager);
// Start server object
ret = pServer->start();

The file servermain.cpp in the C++ SDK Demo Server provides a more complete example for a server main function.

The SDK has three main options to create the application specific nodes for a namespace in the server address space:

Create nodes with custom code
Create every single node with NodeManagerBase::addNodeAndReference() in application specific code. This option is typically used if the address space if created from a vendor specific configuration or from information provided by the underlying system e.g. the capabilities and configuration of a device.
Use generated code from UaModeler
Create code with UaModeler where classes are created for ObjectTypes, VariableTypes and DataTypes. Creating an instance of e.g. an ObjectType handler class creates automatically all necessary nodes. This option can be used for types known at compile time of the server and is simplifying the server implementation, especially if the namespace defines structure DataTypes and ObjectTypes with methods where the business logic for the methods is implemented in the OPC UA server code.
UANodeSet XML import
Load namespace from UANodeSet XML file to create the nodes for this namespace. This option can be used to load nodes that are not known at compile time of the server. The limitations and the solution to work around this limitations is described in the detailed UANodeSet XML import section.

When generated code is used, typically the type namespaces are created with generated code and the instance namespace is created with custom code. The instances are created and initialized from the current configuration of the underlying system.

Generated code and loading with XML can be combined, e.g., a type namespace with ObjectTypes, VariableTypes and DataTypes can be implemented with generated code and an instance namespace with objects and variables can be loaded from XML. This is especially interesting if the types contain business logic that is implemented in the OPC-UA server code.

Create nodes with custom code

Nodes are either created at start-up of the server or after configuration changes. The NodeManagerBase requires that a derived class implements afterStartUp() as shown in the example below. The nodes and references created at start-up of the server should be created in this method.

class MyNodeManager : public NodeManagerBase
{
UA_DISABLE_COPY(MyNodeManager);
public:
MyNodeManager();
virtual ~MyNodeManager();
};

The following sample code shows the creation of a folder object with two variables. The sample code creates three nodes with references to their parent nodes in the tree for the Folder object and for the two BaseDataVariable variables.

UaStatus MyNodeManager::afterStartUp()
{
UaStatus ret;
OpcUa_UInt16 myNamespaceIndex = getNameSpaceIndex();
UaVariant defaultValue;
OpcUa::FolderType *pFolder = NULL;
OpcUa::BaseDataVariableType *pDataVariable = NULL;
// Create a folder with name MyFolder
pFolder = new OpcUa::FolderType(
UaNodeId("MyFolder", myNamespaceIndex), // NodeId of the node
"MyFolder", // Name of the node
myNamespaceIndex, // Namespace index for the browse name
this); // Node manager responsible for the node
// Add the folder object with a reference from the Objects folder defined by OPC UA
ret = addNodeAndReference(OpcUaId_ObjectsFolder, pFolder, OpcUaId_Organizes);
if (ret.isBad()) return ret;
// Create a variable with name MyVariable1 and identifier MyFolder.MyVariable1
defaultValue.setUInt32(0);
pDataVariable = new OpcUa::BaseDataVariableType(
UaNodeId("MyFolder.MyVariable1", myNamespaceIndex), // NodeId of the node
"MyVariable1", // Name of the node
getNameSpaceIndex(), // Namespace index for the browse name
defaultValue, // Initial value and DataType
OpcUa_AccessLevels_CurrentRead, // Access level for the variable
this, // Node manager responsible for the node
pFolder->getSharedMutex()); // Mutex used for the node
// Add the variable with a reference from the folder object
ret = addNodeAndReference(pFolder, pDataVariable, OpcUaId_HasComponent);
if (ret.isBad()) return ret;
// Create and add a second variable with name MyVariable2 and identifier MyFolder.MyVariable2
defaultValue.setDouble(0.0);
pDataVariable = new OpcUa::BaseDataVariableType(
UaNodeId("MyFolder.MyVariable2", myNamespaceIndex),
"MyVariable2", getNameSpaceIndex(),
defaultValue, OpcUa_AccessLevels_CurrentRead,
this, pFolder->getSharedMutex());
ret = addNodeAndReference(pFolder, pDataVariable, OpcUaId_HasComponent);
if (ret.isBad()) return ret;
// Resulting node tree:
//
// |---- Root
// | |---- Objects
// | | |---- MyFolder
// | | | |---- MyVariable1
// | | | |---- MyVariable2
return ret;
}

The ObjectType classes for OPC UA defined types included in the SDK create all mandatory children automatically. This is also true for code generated for ObjectTypes from other namespaces.

The following sample code shows how to create a FileType object with all children.

UaStatus MyNodeManager::afterStartUp()
{
UaStatus ret;
OpcUa_UInt16 myNamespaceIndex = getNameSpaceIndex();
OpcUa::FileType *pMyFile;
// Create a FileType object with all child nodes
pMyFile = new OpcUa::FileType(
UaNodeId("MyFile", myNamespaceIndex), // NodeId of the node
"MyFile", // Name of the node
myNamespaceIndex, // Namespace index for the browse name
this); // Node manager responsible for the node
// Add the FileType object with a reference from the Objects folder defined by OPC UA
ret = addNodeAndReference(OpcUaId_ObjectsFolder, pMyFile, OpcUaId_Organizes);
if (ret.isBad()) return ret;
// Set the information necessary for the business logic of the file object
pMyFile->setFilePath("myfile.txt");
// Resulting node tree:
//
// |---- Root
// | |---- Objects
// | | |---- MyFile
// | | | |---- OpenCount
// | | | |---- Size
// | | | |---- Writable
// | | | |---- UserWritable
// | | | |---- Open()
// | | | |---- Read()
// | | | |---- Write()
// | | | |---- GetPosition()
// | | | |---- SetPosition()
// | | | |---- Close()
return ret;
}

It is also possible to create type nodes like ObjectType, DataType, VariableType or RefrenceType with custom code. This is especially necessary if the types are created during the configuration of the OPC UA server at runtime.

More sample code for generation of nodes with custom code is provided in the server getting started Lesson 2: Extending the Address Space with Real World Data. This includes the creation of custom ObjectTypes and instances of these custom ObjectTypes.

Use generated code from UaModeler

The tool UaModeler is used to generate code for the C++ SDK. UaModeler can either load existing information models by importing the corresponding UANodeSet XML file or own information models can be created using UaModeler. The code generation does not require a license. Nevertheless, the SDK license also contains a UaModeler license necessary for exporting information models to a UANodeSet XML file.

The documentation of UaModeler provides a step by step example on how to create a project and a OPC UA type and how to generate code and integrate it into a server. See UaModeler documentation > HowTo and Short Tutorials > HowTo of C++.

If code is generated for a namespace, all dependent namespaces must also be available as generated code.

The generated code includes a NodeManager for each namespace. The generated NodeManager class is derived from NodeManagerBase.

The code generator creates two classes for NodeManagers and ObjectTypes e.g. for the ObjectType MyObjectType, the two classes MyObjectTypeBase and MyObjectType are generated.

In this example the MyObjectTypeBase class contains the generated OPC UA mapping code. This code is modified if the type is extended and the generated code is updated. The class MyObjectTypeis the implementation class and contains only the generated Method prototypes. These classes are intended for adding application specific code. The base classes like MyObjectTypeBase should not be modified since they would be overwritten by newly generated code. The implementation class requires only an update of the generated code if the Method signatures change.

For structure DataTypes the generated code consist of the corresponding ANSI C structures with the OPC UA binary encoding code and C++ wrapper classes for a single structure or an array of the structure. The wrapper classes provide also helper functions to convert the structure to and from Variant, DataValue or ExtensionObject used for OPC UA communication.

The following sample code shows how to handle an array of the Range structure DataType.

UaRanges rangeArray;
UaVariant rangeArrayValue;
// Create array and set values
rangeArray.create(2);
rangeArray[0].Low = 0.1;
rangeArray[0].High = 9.9;
rangeArray[1].Low = 10;
rangeArray[1].High = 90;
// Create Variant from array
rangeArray.toVariant(rangeArrayValue);

UANodeSet XML import

Nodes can be loaded from UANodeSet XML files. This option can be used to load nodes that are not known at compile time of the server.

Nodes from depending namespaces must be created before loading the XML file. The NodeManagers are started in the order they are added to the server object.

The following sample code shows how to load a UANodeSet XML file during server start.

// Create and initialize server object
OpcServer* pServer = new OpcServer;
// XML UANodeSet file to load
UaString sNodesetFile(UaString("%1/uanodesetimport.xml").arg(szAppPath));
// Create XML parser module
UaNodeSetXmlParserUaNode* pXmlParser = new UaNodeSetXmlParserUaNode(sNodesetFile);
// Add UANodeSet XML parser as module
pServer->addModule(pXmlParser);
// Start server object
ret = pServer->start();

The XML schema defines only the information necessary to create the nodes and references. But the nodes typically represent information in the underlying system. For example, a variable represents the value of a register in the device. The information about that register, such as the address of the register, is not represented in the standard XML schema information.

The XML schema allows adding extensions to nodes with vendor-specific information such as the register address for a variable. The XML parser can use application-specific factories to handle such extensions in the XML file. The C++ SDK Demo Server provides sample code for loading an XML file with extensions. See servermain.cpp as starting point.

If the nodes are loaded from a UANodeSet XML file, the loader checks if there is code for the nodes type definition. This is done through a node Factory for a namespace that is part of the generated code. If there is generated code, the loader creates an instance of the implementation class for the type instead of creating the single nodes for an instance of this type. This happens also if a XML file contains a type that is a subtype of an existing type. For this case the factory is called to create the base type.

If the UANodeSet XML file contains event nodes, then the SDK must be built with support for events (Event Subscription Server Facet). When building the SDK using CMake, the configuration option UASDK_WITH_EVENT_SUBSCRIPTION_SERVER_FACET must be set to ON.

OPC UA Namespace Nodes

The nodes for the OPC UA namespace (index 0) are created by the SDK or there is code in the SDK to create them if needed. It is not possible to load nodes for namespace index 0 from a UANodeSet XML file.

If new OPC UA namespace types are used in UaModeler, it is required to use a SDK that has at least the matching version or a higher version of the OPC UA namespace. It is not enough to update to a UaModler version that supports the feature. It is also necessary to update the SDK version that supports the feature.

The OPC UA namespace is published by the OPC Foundation together with the OPC UA specification as UANodeSet XML file. The SDK is frequently updated to the latest version of the UANodeSet.

The OPC UA namespace contains an ever growing number of nodes and most OPC UA servers use only a small subset of these nodes. Therefore, the SDK does not create all of these nodes in the OPC UA server address space. Especially all nodes related to ObjectTypes and VariableTypes are only created if they are used. This happens automatically as soon as the related implementation class for the type is used e.g. OpcUa::AliasNameCategoryType for the ObjectType AliasNameCategoryType. This happens also if an instance of such a type is created through importing the instances through a UANodeSet XML file.

If the nodes related to a type are not automatically created, they can be created by calling the static createTypes() method on the related class like OpcUa::AliasNameCategoryType::createTypes().

The best time for creating additional OPC UA defined nodes is after the start of the SDK core module but before the application specific NodeManagers are started. To do this it is necessary to implement UaServerApplicationCallback::afterCoreModuleStarted().

In the C++ SDK Demo Server this is done in MyServerCallback that implements the interface UaServerApplicationCallback. See also the sample code below.

The method can also be used to create any node in the OPC UA namespace that is not automatically created by the SDK.

void MyServerCallback::afterCoreModuleStarted()
{
// Create OPC UA namespace nodes here if not created by SDK
// This method is called after the SDK managed NodeManager are started but before
// the application specific NodeManagers are started
// Get the singleton NodeManagerRoot
// Create a node that is not created by the SDK
OpcUaId_Dictionaries,
OpcUa_BrowseName_Dictionaries,
0,
pNMRoot);
pNMRoot->addNodeAndReference(OpcUaId_Server, pDictrionaries, OpcUaId_HasComponent);
// Type nodes can be created with the corresponding implementation classes
// This is normally done when the first instance is created
}