C++ UA Server SDK  1.5.0.318
 All Classes Namespaces Functions Variables Typedefs Enumerations Enumerator Friends Modules Pages
Lesson 3: Connecting the Nodes to Real Time Data

In the previous Lesson 2: Extending the Address Space with Real World Data we created a nice object oriented address space, but the data provided by this address space are only initial values. There is no connection to real time data implemented yet.

If the source of the real time data delivers data changes through an event based mechanism, the connection to the data source is very simple. The only thing that needs to be implemented is the call to UaVariable::setValue on the variable nodes if a data change arrives for this variable. All the read and data monitoring is already handled by the SDK. An example for this update mechanism can be found in the Hello World server example → Create a variable in the server’s address space → Simulate Data.

The application specific node manager implementation gets informed about write action to the variables by overwriting the IOManagerUaNode methods IOManagerUaNode::beforeSetAttributeValue() or IOManagerUaNode::afterSetAttributeValue() in the node manager implementation.

If the source of the real time data requires data polling, this lesson explains the steps necessary to implement read, monitoring and write access to device data.

Step 1: Introducing BaUserData

The read and write access to the node attributes is implemented in the SDK class IOManagerUaNode by accesssing the UaNode interfaces like UaVariable or UaObject. All necessary information is already provided during the creation of the node in NodeManagerUaNode::afterStartUp() or during runtime.

The variable specific value attribute handling is defined by the bit mask returned from UaVariable::valueHandling. There are three options:

UaVariable_Value_Cache | UaVariable_Value_CacheIsSource (Default setting)
This default setting tells the SDK that the value is always up-to-date and all read, write and monitoring actions can be executed on the UaVariable node. This option is used if the variable represents internal configuration data or the data source is event based and delivers data changes automatically.
UaVariable_Value_Cache (Used in this lesson)
This setting is used if the read and write access for this variable will be implemented by overwriting the methods IOManagerUaNode::readValues() and IOManagerUaNode::writeValues() in the node manager implementation.
UaVariable_Value_None
This setting is used if the IOManager interface is implemented directly for this variable. This requires to overwrite NodeManagerUaNode::getIOManager() and to return the own IOManager responsible for the value attribute of this variable.

We need to change all controller variables that provide data from the devices to the option UaVariable_Value_Cache to get the read and write calls for these variables. To execute these read and write actions, we need to store the information to access the device in the variable. This is done by using the UaNode functionality UaNode::setUserData() and UaNode::getUserData(). For this storage capability we need to derive our data class from UserDataBase to allow the node to delete the data object when the node is deleted. The following figure shows the new BaUserData class and how it is related to the other classes.

Figure 3-1 Introducing BaUserData

l4gettingstartedlesson03_bacontrollervariable_classdiagramm.png

UserDataBase Class Definition

Add the class definition of BaUserData to the file controllerobject.h:

#ifndef __CONTROLLEROBJECT_H__
#define __CONTROLLEROBJECT_H__
#include "uaobjecttypes.h"
#include "userdatabase.h" // Add this line
class NmBuildingAutomation;
...
// New code begins
class BaUserData : public UserDataBase
{
UA_DISABLE_COPY(BaUserData);
public:
BaUserData(
OpcUa_Boolean isState,
OpcUa_UInt32 deviceAddress,
OpcUa_UInt32 variableOffset)
: m_isState(isState),
m_deviceAddress(deviceAddress),
m_variableOffset(variableOffset)
{}
virtual ~BaUserData(){}
/* Indicates if this is a state variable. */
inline OpcUa_UInt32 isState() const { return m_isState; }
/* Returns the device address. */
inline OpcUa_UInt32 deviceAddress() const { return m_deviceAddress; }
/* Returns the variable offset in the device. */
inline OpcUa_UInt32 variableOffset() const { return m_variableOffset; }
private:
OpcUa_Boolean m_isState;
OpcUa_UInt32 m_deviceAddress;
OpcUa_UInt32 m_variableOffset;
};
// New code ends
#endif

The class stores the following information:

m_isState
Flag indicating if the variable represents the state of the controller
m_deviceAddress
The device address used to access the device
m_variableOffset
The offset of the variable in the device data area

Step 2: Use of BaUserData in Controller Classes

Two settings are necessary to change the behaviour of a UaVariable to the device read and write actions.

To pass the necessary device address and the communication interface to the controller classes, we need to extend the constructors. The changes are shown in the following code snippets.

Add the marked code to controllerobject.h,

class ControllerObject :
public UaObjectBase
{
UA_DISABLE_COPY(ControllerObject);
public:
ControllerObject(
const UaString& name,
const UaNodeId& newNodeId,
const UaString& defaultLocaleId,
NmBuildingAutomation* pNodeManager, // Line has changed
// New code begins
OpcUa_UInt32 deviceAddress,
BaCommunicationInterface *pCommIf);
// New code ends

airconditionercontrollerobject.h,

class AirConditionerControllerObject :
public ControllerObject
{
public:
AirConditionerControllerObject(
const UaString& name,
const UaNodeId& newNodeId,
const UaString& defaultLocaleId,
NmBuildingAutomation* pNodeManager, // Line has changed
// New code begin
OpcUa_UInt32 deviceAddress,
BaCommunicationInterface *pCommIf);
// New code end

and furnacecontrollerobject.h.

class FurnaceControllerObject :
public ControllerObject
{
public:
FurnaceControllerObject(
const UaString& name,
const UaNodeId& newNodeId,
const UaString& defaultLocaleId,
NmBuildingAutomation* pNodeManager, // Line has changed
// New code begin
OpcUa_UInt32 deviceAddress,
BaCommunicationInterface *pCommIf);
// New code end

The use of the new parameters and the necessary settings to the variable objects is shown in the following code snippet from controllerobject.cpp:

ControllerObject::ControllerObject(
const UaString& name,
const UaNodeId& newNodeId,
const UaString& defaultLocaleId,
NmBuildingAutomation* pNodeManager, // Line has changed
// New code begin
OpcUa_UInt32 deviceAddress,
BaCommunicationInterface *pCommIf)
// New code end
: UaObjectBase(name, newNodeId, defaultLocaleId),
m_pSharedMutex(NULL), // Line has changed
// New code begins
m_deviceAddress(deviceAddress),
m_pCommIf(pCommIf)
// New code ends
{
// Use a mutex shared across all variables of this object
m_pSharedMutex = new UaMutexRefCounted;
UaVariable* pInstanceDeclaration = NULL; // Add "= NULL"
OpcUa::BaseDataVariableType* pDataVariable = NULL; // Add "= NULL"
OpcUa::DataItemType* pDataItem = NULL; // Add "= NULL"
OpcUa::AnalogItemType* pAnalogItem = NULL; // Add "= NULL"
BaUserData* pUserData = NULL; // Add this line
UaStatus addStatus;
/**************************************************************
* Create the Controller components
*/
// Add Variable "State"
// Get the instance declaration node used as base for this variable instance
pInstanceDeclaration = pNodeManager->getInstanceDeclarationVariable(Ba_ControllerType_State);
UA_ASSERT(pInstanceDeclaration!=NULL);
pDataVariable = new OpcUa::BaseDataVariableType(
this, // Parent node
pInstanceDeclaration, // Instance declaration variable this variable instance is based on
pNodeManager, // Node manager responsible for this variable
m_pSharedMutex); // Shared mutex used across all variables of this object
addStatus = pNodeManager->addNodeAndReference(this, pDataVariable, OpcUaId_HasComponent);
UA_ASSERT(addStatus.isGood());
// New code begins
// Store information needed to access device
pUserData = new BaUserData(OpcUa_True, deviceAddress, 0);
pDataVariable->setUserData(pUserData);
// Change value handling to get read and write calls to the node manager
// New code ends
// Add Variable "Temperature"
...
pAnalogItem->setEngineeringUnits(tempEUInformation);
// New code begins
// Store information needed to access device
pUserData = new BaUserData(OpcUa_False, deviceAddress, 0);
pAnalogItem->setUserData(pUserData);
// Change value handling to get read and write calls to the node manager
// New code ends
// Add Variable "TemperatureSetPoint"
...
pAnalogItem->setEngineeringUnits(tempEUInformation);
// New code begins
// Store information needed to access device
pUserData = new BaUserData(OpcUa_False, deviceAddress, 1);
pAnalogItem->setUserData(pUserData);
// Change value handling to get read and write calls to the node manager
// New code ends
// Add Variable "PowerConsumption"
...
UA_ASSERT(addStatus.isGood());
// New code begins
// Store information needed to access device
pUserData = new BaUserData(OpcUa_False, deviceAddress, 2);
pDataItem->setUserData(pUserData);
// Change value handling to get read and write calls to the node manager
// New code ends

The two setting calls on the variables are added to each variable instance of the controller base class in the constructor of the class ControllerObject.

Please note the different device offsets used in the examples above. The offset is based on the knowledge about the controller device.

Add the marked code changes to airconditionercontrollerobject.cpp

AirConditionerControllerObject::AirConditionerControllerObject(
const UaString& name,
const UaNodeId& newNodeId,
const UaString& defaultLocaleId,
NmBuildingAutomation* pNodeManager, // Line has changed
// New code begins
OpcUa_UInt32 deviceAddress,
BaCommunicationInterface *pCommIf)
// New code ends
: ControllerObject(name, newNodeId, defaultLocaleId, pNodeManager, deviceAddress, pCommIf) // Add parameters deviceAddress, pCommIf
{
UaVariable* pInstanceDeclaration = NULL; // Add "=NULL"
OpcUa::AnalogItemType* pAnalogItem = NULL; // Add "=NULL"
BaUserData* pUserData = NULL; // Add this line
UaStatus addStatus;
/**************************************************************
* Create the AirConditionerController components
*/
// Add Variable "Humidity"
...
pAnalogItem->setEngineeringUnits(tempEUInformation);
// New code begins
// Store information needed to access device
pUserData = new BaUserData(OpcUa_False, deviceAddress, 3);
pAnalogItem->setUserData(pUserData);
// Change value handling to get read and write calls to the node manager
// New code ends
// Add Variable "HumiditySetpoint"
...
pAnalogItem->setEngineeringUnits(tempEUInformation);
// New code begins
// Store information needed to access device
pUserData = new BaUserData(OpcUa_False, deviceAddress, 4);
pAnalogItem->setUserData(pUserData);
// Change value handling to get read and write calls to the node manager
// New code ends

The two setting calls on the variables are also added to each variable instance of the air conditioner controller class in the constructor of the class AirConditionerControllerObject.

Add the marked code changes to furnacecontrollerobject.cpp

FurnaceControllerObject::FurnaceControllerObject(
const UaString& name,
const UaNodeId& newNodeId,
const UaString& defaultLocaleId,
NmBuildingAutomation* pNodeManager, // Line has changed
// New code begins
OpcUa_UInt32 deviceAddress,
BaCommunicationInterface *pCommIf)
// New code ends
: ControllerObject(name, newNodeId, defaultLocaleId, pNodeManager, deviceAddress, pCommIf) // Add parameters pNodeManager, pCommIf
{
UaVariable* pInstanceDeclaration = NULL; // Add "=NULL"
OpcUa::DataItemType* pDataItem = NULL; // Add "=NULL"
BaUserData* pUserData = NULL; // Add this line
UaStatus addStatus;
/**************************************************************
* Create the FurnaceController components
*/
// Add Variable "GasFlow"
....
UA_ASSERT(addStatus.isGood());
// New code begins
// Store information needed to access device
pUserData = new BaUserData(OpcUa_False, deviceAddress, 3);
pDataItem->setUserData(pUserData);
// Change value handling to get read and write calls to the node manager
// New code ends

The two setting calls on the variables are also added to each variable instance of the furnace controller class in the constructor of the class FurnaceControllerObject.

Step 3: Implementing IOManagerUaNode Functionality

After changing the UaVariable settings, the method IOManagerUaNode::readValues is called for UA Read service invocations and data monitoring and the method IOManagerUaNode::writeValues is called for UA Write service invocations. We need to overwrite these methods in the class NmBuildingAutomation since the implementation in IOManagerUaNode is empty.

Figure 3-2 Overwriting IOManagerUaNode methods for read and write

l4gettingstartedlesson03_iomanageruanode_classdiagramm.png

NmBuildingAutomation Class Definition

NmBuildingAutomation has to overwrite readValues() and writeValues() shown in the following class definition. We also need to add the member variable for the communication interface pointer.

#ifndef __NMBUILDINGAUTOMATION_H__
#define __NMBUILDINGAUTOMATION_H__
#include "nodemanagerbase.h"
// New code begins
class BaCommunicationInterface;
// New code ends
class NmBuildingAutomation : public NodeManagerBase
{
UA_DISABLE_COPY(NmBuildingAutomation);
public:
NmBuildingAutomation();
virtual ~NmBuildingAutomation();
// NodeManagerUaNode implementation
// New code begins
// IOManagerUaNode implementation
virtual UaStatus readValues(const UaVariableArray &arrUaVariables, UaDataValueArray &arrDataValues);
virtual UaStatus writeValues(const UaVariableArray &arrUaVariables, const PDataValueArray &arrpDataValues, UaStatusCodeArray &arrStatusCodes);
// New code ends
UaVariable* getInstanceDeclarationVariable(OpcUa_UInt32 numericIdentifier);
private:
UaStatus createTypeNodes();
// New code begins
BaCommunicationInterface *m_pCommIf;
// New code ends
};
#endif // __NMBUILDINGAUTOMATION_H__

NmBuildingAutomation Class Implementation

The following code additions to nmbuildingautomation.cpp provides the method prototypes and the creation of the communication interface, BaCommunicationInterface, which is introduced in the next step.

#include "nmbuildingautomation.h"
#include "buildingautomationtypeids.h"
#include "airconditionercontrollerobject.h"
#include "furnacecontrollerobject.h"
#include "opcua_analogitemtype.h"
#include "bacommunicationinterface.h" // Add this line
NmBuildingAutomation::NmBuildingAutomation()
: NodeManagerBase("urn:UnifiedAutomation:CppDemoServer:BuildingAutomation")
{
// New code begins
m_pCommIf = new BaCommunicationInterface;
// New code ends
}
NmBuildingAutomation::~NmBuildingAutomation()
{
// New code begins
delete m_pCommIf;
// New code ends
}
...
// New code begins
UaStatus NmBuildingAutomation::readValues(const UaVariableArray &arrUaVariables, UaDataValueArray &arrDataValues)
{
UaStatus ret;
return ret;
}
UaStatus NmBuildingAutomation::writeValues(const UaVariableArray &arrUaVariables, const PDataValueArray &arrpDataValues, UaStatusCodeArray &arrStatusCodes)
{
UaStatus ret;
return ret;
}
// New code ends
UaStatus NmBuildingAutomation::createTypeNodes()
{
...

For now, we leave the function bodies almost empty due to the lack of a communication interface for getting the information to access devices. The communication interface is introduced in the next step.

Step 4: Introducing BaCommunicationInterface

The remaining part of this lesson aims on linking IOManagerUaNode functionality with device data. Therefore, we introduce the class BaCommunicationInterface, a class being prepared for providing simulation data for our scenario through the interface shown in Figure 3-3.

Figure 3-3 BaCommunicationInterface

l4gettingstartedlesson03_bacommunicationinterface.png

Whereas

getCountControllers()
returns the number of available controllers
getControllerConfig()
provides configuration information of a controller, e.g. address information and type
getControllerState()
provides the current state of a controller
getControllerData()
allows read access to a certain data point in a controller with controller index and data offset in controller
setControllerData()
allows write access to a certain data point in a controller with controller index and data offset in controller

To use the class BaCommunicationInterface and the simulation data, add the following files to your project:

  • [SDK Installation Directory]/examples/simulation_buildingautomation/bacommunicationinterface.cpp
  • [SDK Installation Directory]/examples/simulation_buildingautomation/bacommunicationinterface.h
  • [SDK Installation Directory]/examples/simulation_buildingautomation/bacontrollersimulation.cpp
  • [SDK Installation Directory]/examples/simulation_buildingautomation/bacontrollersimulation.h

Add the marked code to controllerobject.h

#ifndef __CONTROLLEROBJECT_H__
#define __CONTROLLEROBJECT_H__
#include "uaobjecttypes.h"
#include "userdatabase.h"
class NmBuildingAutomation;
class BaCommunicationInterface; // Add this line
class ControllerObject :
public UaObjectBase
{
UA_DISABLE_COPY(ControllerObject);
public:
ControllerObject(
const UaString& name,
const UaNodeId& newNodeId,
const UaString& defaultLocaleId,
NmBuildingAutomation* pNodeManager,
OpcUa_UInt32 deviceAddress,
BaCommunicationInterface *pCommIf);
virtual ~ControllerObject(void);
OpcUa_Byte eventNotifier() const;
protected:
UaMutexRefCounted* m_pSharedMutex;
OpcUa_UInt32 m_deviceAddress; // Add this line
BaCommunicationInterface* m_pCommIf; // Add this line
};
...

Step 5: Implementing NmBuildingAutomation::readValues() for Non-State Variables

In the next step, we are implementing NmBuildingAutomation::readValues() for Non-State Variables.

UaStatus NmBuildingAutomation::readValues(
const UaVariableArray &arrUaVariables,
UaDataValueArray &arrDataValues)
{
UaStatus ret;
// New code begins
OpcUa_UInt32 i;
OpcUa_UInt32 count = arrUaVariables.length();
UaDateTime timeStamp = UaDateTime::now();
// Create result array
arrDataValues.create(count);
for (i=0; i<count; i++)
{
// Set timestamps
arrDataValues[i].setSourceTimestamp(timeStamp);
arrDataValues[i].setServerTimestamp(timeStamp);
// Cast UaVariable to BaControllerVariable
UaVariable* pVariable = arrUaVariables[i];
if (pVariable)
{
BaUserData* pUserData = (BaUserData*)pVariable->getUserData();
if ( pUserData )
{
UaVariant vTemp;
UaStatusCode status;
if ( pUserData->isState() == OpcUa_False )
{
// Read of a data variable
OpcUa_Double value;
// Get the data for the controller from the communication interface
status = m_pCommIf->getControllerData(
pUserData->deviceAddress(),
pUserData->variableOffset(),
value);
if ( status.isGood() )
{
// Set value
vTemp.setDouble(value);
arrDataValues[i].setValue(vTemp, OpcUa_True, OpcUa_False);
}
else
{
// Set Error
arrDataValues[i].setStatusCode(status.statusCode());
}
}
else
{
arrDataValues[i].setStatusCode(OpcUa_BadNotImplemented);
}
}
else
{
arrDataValues[i].setStatusCode(OpcUa_BadInternalError);
}
}
else
{
arrDataValues[i].setStatusCode(OpcUa_BadInternalError);
}
}
// New code ends
return ret;
}

readValues() provides an array of UaVariable interface pointers used to indicate which variables should be read. The second parameter is an array of UaDataValue classes used to return the values read.

After creating the output array for the read values, the requested variables are processed in a loop.

For every variable, the user data is requested by using the method UaNode::getUserData(). The returned user data object is casted to BaUserData. The user data is then used to detect if the state or one of the data variables is requested.

If a data value is requested, the method getControllerData of the communication interface is called. The device address and the offset stored in the user data are used to get the current value. The returned value or, in the case of an error, the status are used to set the data value in the out parameter array.

Step 6: Creating Devices Based on BaCommunicationInterface Information

We can also use BaCommunicationInterface in order to create devices in NmBuildingAutomation::afterStartUp().

Therefore, we need the number of available controllers provided by getControllerCount(), and additional configuration information, especially the type of each controller and its address provided by getControllerConfig().

We replace the method NmBuildingAutomation::afterStartUp with the following code.

UaStatus NmBuildingAutomation::afterStartUp()
{
UaStatus ret;
UaFolder *pFolder = NULL;
AirConditionerControllerObject *pAirConditioner = NULL;
FurnaceControllerObject *pFurnace = NULL;
UaString sControllerName;
OpcUa_UInt32 count = m_pCommIf->getCountControllers(); // Get the count of configured controllers
OpcUa_UInt32 i;
OpcUa_UInt32 controllerAddress;
BaCommunicationInterface::ControllerType controllerType;
createTypeNodes();
/**************************************************************
Create a folder for the controller objects and add the folder to the ObjectsFolder
*/
pFolder = new UaFolder("BuildingAutomation", UaNodeId("BuildingAutomation", getNameSpaceIndex()), m_defaultLocaleId);
ret = addNodeAndReference(OpcUaId_ObjectsFolder, pFolder, OpcUaId_Organizes);
/**************************************************************
* Create the Controller Object Instances
*/
for ( i=0; i<count; i++ )
{
ret = m_pCommIf->getControllerConfig(
i,
controllerType,
sControllerName,
controllerAddress);
if ( controllerType == BaCommunicationInterface::AIR_CONDITIONER )
{
pAirConditioner = new AirConditionerControllerObject(
sControllerName,
UaNodeId(sControllerName, getNameSpaceIndex()),
m_defaultLocaleId,
this,
controllerAddress,
m_pCommIf);
ret = addNodeAndReference(pFolder, pAirConditioner, OpcUaId_Organizes);
UA_ASSERT(ret.isGood());
}
else
{
pFurnace = new FurnaceControllerObject(
sControllerName,
UaNodeId(sControllerName, getNameSpaceIndex()),
m_defaultLocaleId,
this,
controllerAddress,
m_pCommIf);
ret = addNodeAndReference(pFolder, pFurnace, OpcUaId_Organizes);
UA_ASSERT(ret.isGood());
}
}
return ret;
}

getCountControllers() provides the number of configured controllers. This number is used to iterate over the controller list and to create the controller objects based on the returned configuration.

getControllerConfig() returns the configuration for a controller based on the passed index. It returns the controller type, name, and address. The type of the controller indicates whether we have to create an air conditioner controller or a furnace controller. The name and the address are used to intialize the object to create.

Step 7: Extending NmBuildingAutomation::readValues() to Access State Variables

Since afterStartUp() has been completed, we need to complete the readValues() implementation by also handling the read access to the State Variables. In order to achieve this, we have to replace

else
{
arrDataValues[i].setStatusCode(OpcUa_BadNotImplemented);
}

with

else
{
// Read of a state variable
// We need to get the state of the controller
BaCommunicationInterface::ControllerState state;
// Get the data for the controller from the communication interface
status = m_pCommIf->getControllerState(
pUserData->deviceAddress(),
state);
if ( status.isGood() )
{
// Set value
vTemp.setUInt32(state);
arrDataValues[i].setValue(vTemp, OpcUa_True, OpcUa_False);
}
else
{
// Set Error
arrDataValues[i].setStatusCode(status.statusCode());
}
}

The read value for the state can be received via BaCommunicationInterface::getControllerState() which takes a controller index (like getControllerData()) and returns the state of the controller.

Step 8: Implementing NmBuildingAutomation::writeValues()

We still have to implement NmBuildingAutomation::writeValues() in order to finish this lesson.

UaStatus NmBuildingAutomation::writeValues(
const UaVariableArray &arrUaVariables,
const PDataValueArray &arrpDataValues,
UaStatusCodeArray &arrStatusCodes)
{
UaStatus ret;
// New code begins
OpcUa_UInt32 i;
OpcUa_UInt32 count = arrUaVariables.length();
// Create result array
arrStatusCodes.create(count);
for ( i=0; i<count; i++ )
{
// Cast UaVariable to BaControllerVariable
UaVariable* pVariable = arrUaVariables[i];
if ( pVariable )
{
BaUserData* pUserData = (BaUserData*)pVariable->getUserData();
if ( pUserData )
{
if ( pUserData->isState() == OpcUa_False )
{
UaVariant vTemp(arrpDataValues[i]->Value);
UaStatusCode status;
OpcUa_Double value;
status = vTemp.toDouble(value);
if ( status.isGood() )
{
// Get the data for the controller from the communication interface
status = m_pCommIf->setControllerData(
pUserData->deviceAddress(),
pUserData->variableOffset(),
value);
}
arrStatusCodes[i] = status.statusCode();
}
else
{
// State variable can not be written
arrStatusCodes[i] = OpcUa_BadNotWritable;
}
}
else
{
arrStatusCodes[i] = OpcUa_BadInternalError;
}
}
else
{
arrStatusCodes[i] = OpcUa_BadInternalError;
}
}
// New code ends
return ret;
}

writeValues() provides an array of UaVariable interface pointers used to indicate which variables should be written. The second parameter is an array of OpcUa_DataValue pointers containing the values to be written. This method returns an array of OpcUa_StatusCode values indicating the success for every requested variable.

After creating the output array for the write results, the requested variables are processed in a loop.

For every variable, the user data is requested by using the method UaNode::getUserData(). The returned user data object is casted to BaUserData. The user data is then used to detect if the state or one of the data variables is requested.

If the state is requested, an error OpcUa_BadNotWritable is returned.

If a data value is requested, the method setControllerData of the communication interface is called. The device address and the offset stored in the user data are used to set the new value. The returned status is set to the corresponding out parameter array element.

Step 9: Run Application

Compile and run the server application.

When connecting to the server with UaExpert, the simulated devices show up in the Address Space. Drag and drop some variables to the Default DA View window to monitor their values (see screenshot).

Figure 3-4 Simulated controllers in UaExpert

serverlesson03_expert.png