UA Server SDK C++ Bundle  1.4.0.258
 All Classes Namespaces Functions Variables Typedefs Enumerations Enumerator Friends Groups Pages
C++ SDK Demo Server

The C++ SDK Demo Server is a collection of examples for developing different features of an OPC UA server. It is mainly composed of the final results of the Server Getting Started lessons with instance nodes in the BuildingAutomation folder and the Unified Automation Demo address space with nodes in the Demo folder.

The complete project can be found in Examples → Server Cpp Demo.

Examples for the following features are included in the demo server:

User Authentication example

There are several steps necessary to implement user authentication and user authorization. The provided sample code for user authentication in the first three steps provides a lot of reusable code. User authorization shown in the rest of the example is very application specific and the example shows one simplified option.

The example for user authentication is applied to the Unified Automation Demo address space. Depending on the user, access to nodes in Objects → Demo → 005_AccessRights is limited. The server has the three users John, Operator and Admin. The password equals user name. John and Operator are members of the Operators group. Admin is member of the Administrators group.

Steps in the example are:

  1. An application specific session class derived from UaSession must be implemented. It must store what ever is needed later on for user authorization. This could be a role, the user name, a user group or the OPC UA rights the user have. The example provides a class MySession that stores a role and the user name. It is implemented in the files mysession.h and mysession.cpp.
  2. ServerConfig::createSession() must be implemented to create your application specific session object and ServerConfig::logonSessionUser() must be implemented to authenticate the user and to store the user context in the session object. Since the helper class OpcServer is used to implement the main server entry, the callback interface OpcServerCallback must be implemented to provide this functionality. The class MyServerCallback implements the callback in the files myservercallback.h and myservercallback.cpp.
  3. You need to set the callback in the OpcServer object. This happens in the file servermain.cpp after creating the OpcServer object.
  4. User Authorization: All SDK interface calls triggered by an OPC UA client service call have the Session object as parameter. You can cast the Session to your session class (like MySession) and get all user information you need to decide if the user is allowed to access information.

On the toolkit layer used in example there are different methods you can overwrite or callbacks you can implement to influence behaviour based on the user context. These overwrites are implemented in the class NodeManagerImpl (see files demo_nodemanagerimpl.h and demo_nodemanagerimpl.cpp).

The following methods overwrite default functionality in the class NodeManagerImpl

  • browseNode() and returnBrowseResultNode() implement the interface BrowseUaNodeCallback defined by NodeManagerUaNode. They allow restricting browse results.
  • beforeSetAttributeValue() allows to restrict write operations
  • afterGetAttributeValue() allows to restrict read operations
  • beforeMonitorAttributeValue() allows to restrict monitoring of nodes

Sampling on request

Overview

An example on how to update variable values is already shown in the building automation server introduced in the Server Getting Started Tutorials. This section describes a different approach.

In this tutorial, the variable value handling is set to UaVariable_Value_Cache to indicate that the value needs to be polled through readValues() if a client is interested in the latest value. The method readValues() is called by the SDK for a Read service call and value change checks for monitored items.

If a server should decide whether a value is checked for changes, the value handling of these variables can be set to UaVariable_Value_CacheIsSource | UaVariable_Value_CacheIsUpdatedOnRequest. In this case, the implementer is informed about changes in monitoring and can implement his or her own logic for checking currently monitored variables for changes. The methods readValues() and writeValues() are still called for Read and Write service calls from clients.

The following code snippets show the differences to the tutorial code and can be found in the following files

  • controllerobject.cpp
  • nmbuildingautomation.h
  • nmbuildingautomation.cpp

Alterations are marked by comments in the following form:

// SamplingOnRequestExample change begin
...
// SamplingOnRequestExample change end

Implementation

Replace the following line in the file controllerobject.cpp

// Change value handling to get read and write calls to the node manager

by this code snippet:

// Set value handling to handle sampling on request
// Change value of variable to bad status BadWaitingForInitialData
// This makes sure we do not deliver an old value before we update the cache with internal monitoring
UaDataValue badStatusValue;
badStatusValue.setStatusCode(OpcUa_BadWaitingForInitialData);
pAnalogItem->setValue(NULL, badStatusValue, OpcUa_False);

The most important change in nmbuildingautomation.h is to overwrite IOManagerUaNode::variableCacheMonitoringChanged(). This method informs the derived class that the monitoring for a variable has been changed.

In addition, the class is derived from UaThread to implement the sampling in a background worker thread. The other additions, like the method run() or the member variables, are necessary to sample the variables which are active in monitoring.

class NmBuildingAutomation :
// SamplingOnRequestExample change begin
// Added: Worker thread to execute the sampling
public UaThread,
// SamplingOnRequestExample change end
{
UA_DISABLE_COPY(NmBuildingAutomation);
public:
NmBuildingAutomation();
virtual ~NmBuildingAutomation();
....
// SamplingOnRequestExample change begin
// Added: Overwrite of function variableCacheMonitoringChanged() to get informed by NodeManagerBase
void variableCacheMonitoringChanged(UaVariableCache* pVariable, TransactionType transactionType);
// Added: Main function for worker thread used to execute the sampling
void run();
// SamplingOnRequestExample change end
...
private:
...
// SamplingOnRequestExample change begin
// Added: Member variables for internal sampling in worker thread
bool m_stopThread;
UaMutex m_mutexMonitoredVariables;
bool m_changedMonitoredVariables;
std::map<UaVariableCache*, UaVariableCache*> m_mapMonitoredVariables;
UaVariableArray m_arrayMonitoredVariables;
// SamplingOnRequestExample change end
};

The new members are initialized in the constructor of the class NmBuildingautomation():

NmBuildingAutomation::NmBuildingAutomation()
: NodeManagerBase("urn:UnifiedAutomation:CppDemoServer:BuildingAutomation", OpcUa_True),
// SamplingOnRequestExample change begins
// Initialization of new members
m_stopThread(false),
m_changedMonitoredVariables(false)
// SamplingOnRequestExample change ends
{
m_pCommIf = new BaCommunicationInterface;
...

Start the sampling worker thread in NmBuildingautomation::afterStartUp():

...
// SamplingOnRequestExample change begins
// Start worker thread
start();
// SamplingOnRequestExample change ends
return ret;
}

Stop the sampling worker thread in NmBuildingAutomation::beforeShutDown():

UaStatus NmBuildingAutomation::beforeShutDown()
{
UaStatus ret;
#if SUPPORT_Historical_Access
m_pHistoryManager->shutDown();
#endif // SUPPORT_Historical_Access
// SamplingOnRequestExample change begins
// Stop worker thread
m_stopThread = true;
// Wait for thread completion
wait();
// SamplingOnRequestExample change ends
return ret;
}

To configure the sampling in the worker thread we have to implement variableCacheMonitoringChanged(). Based on the UaVariableCache::signalCount() the variable is added to sampling if the first monitored item is created, and removed from sampling if the last monitored item is removed. UaVariableCache::getMinSamplingInterval() returns the shortest sampling interval currently used for the variable. This information is not used in this example but it may be used if variables can be sampled with different rates.

/* Overwrite of base class function to get informed by NodeManagerBase about a change in monitoring
*/
void NmBuildingAutomation::variableCacheMonitoringChanged(UaVariableCache* pVariable, TransactionType transactionType)
{
// Just make sure only handle the right variables
{
return;
}
// Get fastest requested sampling interval requested by a client
// Can be used to change polling rate to device if fastest rate changed
OpcUa_Double fastedRequestedRate = pVariable->getMinSamplingInterval();
OpcUa_ReferenceParameter(fastedRequestedRate);
// This is not used in this example
if ( (transactionType == IOManager::TransactionMonitorBegin) && (pVariable->signalCount() == 1) )
{
// The first monitored item was created for variable (pVariable)
// Lock access to variable list
UaMutexLocker lock(&m_mutexMonitoredVariables);
// Add to map and set changed flag
m_mapMonitoredVariables[pVariable] = pVariable;
m_changedMonitoredVariables = true;
// Increment reference counter for the entry in the map
pVariable->addReference();
}
else if ( (transactionType == IOManager::TransactionMonitorStop) && (pVariable->signalCount() == 0) )
{
// The last monitored item was removed for variable (pVariable)
// Lock access to variable list
UaMutexLocker lock(&m_mutexMonitoredVariables);
// Add to map and set changed flag
std::map<UaVariableCache*, UaVariableCache*>::iterator it = m_mapMonitoredVariables.find(pVariable);
if ( it != m_mapMonitoredVariables.end() )
{
m_mapMonitoredVariables.erase(it);
m_changedMonitoredVariables = true;
// Decrement reference counter since we removed the entry from the map
pVariable->releaseReference();
}
}
}

The internal sampling is implemented in the main method of the worker thread:

void NmBuildingAutomation::run()
{
UaStatus ret;
OpcUa_UInt32 i;
OpcUa_UInt32 count;
std::map<UaVariableCache*, UaVariableCache*>::iterator it;
while ( m_stopThread == false )
{
// Lock access to variable list
UaMutexLocker lock(&m_mutexMonitoredVariables);
// Check if the list was changed
if ( m_changedMonitoredVariables )
{
// Update list for sampling
// First release reference for all variables in array
count = m_arrayMonitoredVariables.length();
for ( i=0; i<count; i++ )
{
// Check if the variable is still used
it = m_mapMonitoredVariables.find((UaVariableCache*)m_arrayMonitoredVariables[i]);
if ( it == m_mapMonitoredVariables.end() )
{
// Change value of variable to bad status BadWaitingForInitialData - it is not longer used
// This makes sure we do not deliver an old value when the monitoring is activated later for this variable
UaDataValue badStatusValue;
badStatusValue.setStatusCode(OpcUa_BadWaitingForInitialData);
m_arrayMonitoredVariables[i]->setValue(NULL, badStatusValue, OpcUa_False);
}
// Decrement reference counter for the variable - we removed it from the list
m_arrayMonitoredVariables[i]->releaseReference();
}
// And clear old array
m_arrayMonitoredVariables.clear();
// Create the new array and increment reference counter for added variables
it = m_mapMonitoredVariables.begin();
m_arrayMonitoredVariables.create(m_mapMonitoredVariables.size());
count = m_arrayMonitoredVariables.length();
for ( i=0; i<count; i++ )
{
m_arrayMonitoredVariables[i] = it->first;
// Increment reference counter - it was added to the list
m_arrayMonitoredVariables[i]->addReference();
it++;
}
// Reset the change flag
m_changedMonitoredVariables = false;
}
lock.unlock();
// Check if we have anything to sample
if ( m_arrayMonitoredVariables.length() > 0 )
{
count = m_arrayMonitoredVariables.length();
// Call readValues to update variable values
ret = readValues(m_arrayMonitoredVariables, results);
if ( ret.isGood() )
{
// Update values
for ( i=0; i<count; i++ )
{
m_arrayMonitoredVariables[i]->setValue(NULL, results[i], OpcUa_True);
}
}
else
{
// Set bad status for all variables
UaDataValue badStatusValue;
badStatusValue.setStatusCode(ret.statusCode());
for ( i=0; i<count; i++ )
{
m_arrayMonitoredVariables[i]->setValue(NULL, badStatusValue, OpcUa_True);
}
}
}
}
}

Loading address space from UANodeSet XML file

The demo server contains an example for loading all or part of the address space from a UANodeSet XML file.

The sample code can be found in

  • servermain.cpp
  • mynodemanagernodesetxmlcreator.h
  • mynodemanagernodesetxmlcreator.cpp
  • mynodemanagernodesetxml.h
  • mynodemanagernodesetxml.cpp
  • buildingautomationxml.xml

The example XML file is located in the directory “bin” of the demo server executable. The file buildingautomationxml.xml contains the same controller objects as the building automation server introduced in the Server Getting Started Tutorials. Some of the sample code to connect the loaded variables to the controller simulation is similar to the code used in the tutorials.

The parser and the necessary classes are initialized and added with the following code in the file servermain.cpp

// Create and initialize server object
OpcServer* pServer = new OpcServer;
pServer->setServerConfig(sConfigFileName, szAppPath);
...
// XML UANodeSet file to load
UaString sNodesetFile(UaString("%1/buildingautomationxml.xml").arg(szAppPath));
// We create our own BaseNode factory to create the user data from XML
MyBaseNodeFactory* pBaseNodeFactory = new MyBaseNodeFactory;
// We create our own NodeManager creator to instantiate our own NodeManager
MyNodeManagerNodeSetXmlCreator* pNodeManagerCreator = new MyNodeManagerNodeSetXmlCreator;
UaNodeSetXmlParserUaNode* pXmlParser = new UaNodeSetXmlParserUaNode(sNodesetFile, pNodeManagerCreator, pBaseNodeFactory, NULL);
// Add UANodeSet XML parser as module
pServer->addModule(pXmlParser);
// Start server object
ret = pServer->start();

The class MyBaseNodeFactory is derived from UaBase::BaseNodeFactory and overwrites the method UaBase::BaseNodeFactory::createVariable(). The factory creates data classes for the OPC UA nodes in the XML file. These data classes are used in a second step to create the nodes in the NodeManager. Overwriting the factory allows the creation of specialized data classes. In the example the data class MyVariable derived from UaBase::Variable is used to parse the extension in the XML file that contains the user data for the variable.

The class MyNodeManagerNodeSetXmlCreator is used to create specialized NodeManagers for a known namespace in the XML file. In this example the special NodeManager is implemented in the class MyNodeManagerNodeSetXml. This class implements all logic necessary to initialize the controller objects and to implement data access to variable values and methods as well as event and alarm handling.

Alarm object handling with and without nodes in the address space

The demo address space code contains sample code for the handling of alarm objects of different types. The example includes alarm objects which show up as OPC UA nodes in the address space as well as alarm objects not visible in the address space. The latter are working completely without nodes in the address space.

The sample code is contained in the class NodeManagerDemo which can be found in the files

  • demo_nodemanagerdemo.h
  • demo_nodemanagerdemo.cpp

The alarm objects are created and initialized in the method createAlarmNodes(). The objects visible in the address space are also added to the NodeManager. These objects are deleted by the NodeManager at shutdown. In addition this method creates variables used to trigger the alarm states.

The alarm objects without nodes have to be deleted in the destructor of the NodeManagerDemo.

The state of the alarm objects can be activated by writing true to the OffNormalAlarm variables or by assigning analog values for the level alarms ranging from 0 to 100. Values below 30 trigger a low alarm. Values above 70 trigger a high alarm. To modify the alarm states on write, the method IOManagerUaNode::afterSetAttributeValue() is overwritten in NodeManagerDemo. This method contains the sample code for changing the alarm states.

To handle alarm acknowledgement the methods EventManagerUaNode::OnAcknowledge and EventManagerUaNode::OnConfirm are overwritten in the class NodeManagerDemo. This works only for alarm objects visible in the address space. To handle the Acknowledge and AddComment methods for alarm objects without nodes, the methods getMethodHandle() and beginCall() are implemented to provide the MethodManager functionality for these alarm objects.

Structure data type example

The demo address space code contains sample code for the handling of structure data types. A variable with a nested structure can be found in Objects -> BuildingAutomation -> ControllerConfigurations. This variable contains a structure with two arrays of structures with the configuration parameters for all controllers.

The sample code can be found in the file

  • nmbuildingautomation.cpp

The enumeration and structure data type nodes and dictionary are created in the function NmBuildingAutomation::createTypeNodes().

The structure value is filled up in the function NmBuildingAutomation::readValues().

Event history access example

The demo address space code contains sample code for the handling of historical access for events. The area object providing event history can be found in Objects -> Server -> AreaAirConditioner. This object event notifier attribute indicates availablity of event history.

The sample code can be found in the files

  • nmbuildingautomation.cpp
  • historymanagercache.h
  • historymanagercache.cpp

The internal event monitored item is created in the function NmBuildingAutomation::afterStartUp().

The class HistoryManagerCache implements all data and event history functions and data and event historizing. The events and data changes are monitored with internal monitored items and are stored in memory.