.NET Based OPC UA Client/Server SDK  3.0.0.439
Lesson 3: Connecting the Nodes to Real Time Data

Introduction

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 update the value of the Variable node if a data change arrives for this variable. All the read and data monitoring is already handled by the SDK.

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.

The following figure shows the example communication interface used for polling access to the simulated device data.

serverlesson03_comm_interface_for_devices.png
Figure 3.1: Communication Interface for Devices

Methods of the UnderlyingSystem

GetBlocks
Number of available controllers and their configuration through BlockConfiguration and BlockProperty classes

Runtime Data

Read
Read block property
Write
Write block property
Start
Start controller
Stop
Stop controller

Integration of the Device Interface

In a first step the example device interface including the device simulation must be added to the project.

Add system as a member of the class.

#region Private Fields
private UnderlyingSystem m_system;
#endregion

Create the system in the constructor of the NodeManager

public Lesson03NodeManager(ServerManager server) : base(server)
{
m_system = new UnderlyingSystem();
}

Initialize the underlying system

public override void Startup()
{
try
{

...

// load the model.
Console.WriteLine("Loading the Controller Model.");
ImportUaNodeset(Assembly.GetEntryAssembly(), "buildingautomation.xml");
// initialize the underlying system.
m_system.Initialize();

Creation of Controller Instances from Device Interface Information

In the previous lesson a fixed set of controllers has been created. This hardcoded object generation will be replaced with information about available controllers provided by the device interface.

// Create a Folder for Controllers

...

// Create controllers from configuration
foreach (BlockConfiguration block in m_system.GetBlocks())
{
// set type definition NodeId
NodeId typeDefinitionId = ObjectTypeIds.BaseObjectType;
if (block.Type == BlockType.AirConditioner)
{
typeDefinitionId = new NodeId(yourorganisation.BA.ObjectTypes.AirConditionerControllerType, TypeNamespaceIndex);
}
else if (block.Type == BlockType.Furnace)
{
typeDefinitionId = new NodeId(yourorganisation.BA.ObjectTypes.FurnaceControllerType, TypeNamespaceIndex);
}
// create object.
settings = new CreateObjectSettings()
{
ParentNodeId = new NodeId("Controllers", InstanceNamespaceIndex),
ReferenceTypeId = ReferenceTypeIds.Organizes,
RequestedNodeId = new NodeId(block.Name, InstanceNamespaceIndex),
BrowseName = new QualifiedName(block.Name, TypeNamespaceIndex),
TypeDefinitionId = typeDefinitionId
};
CreateObject(Server.DefaultRequestContext, settings);

serverlesson03_address_space.png
Figure 3.2: Result in the server’s address space

Connect Variable Value Attribute to Real Time Data

Our lesson specific NodeManager is derived from BaseNodeManager class. The project specific information integration is done by overwriting methods of the base class. Startup is overwritten to create the address space. For polling based data integration, we need to overwrite the methods Read and Write.

serverlesson03_iomanager.png
Figure 3.3

The value attribute of a variable node can be provided in different ways based on the source of the value. The source can be the in memory node like for server configuration data or it could be in an external device.

Depending on the type of source and communication, different modes can be configured for the variable. For Var1 the mode NodeHandleType.Internal would be used. For Var2 the mode NodeHandleType.ExternalPush would be used. Var3 matches our example and mode NodeHandleType.ExternalPolled is used. The different modes are described in the overview for Data Access, Handle Types and the IIOManager.

serverlesson03_var_value_handling.png
Figure 3.4

In the next steps we need to change variable value handling setting to NodeHandleType.ExternalPolled and we need to provide the address information in variable user data.

In addition, we need to implement Read and Write in the Lesson NodeManager. Read and Write are defined by BaseNodeManager and are overwritten in the Lesson NodeManager.

In a first step, we define the data class for variable user data in the Lesson NodeManager. The Address is used to address the controller. The Offset is used to address the variable inside the controller.

#region SystemAddress Class
private class SystemAddress
{
public int Address;
public int Offset;
}
#endregion

After the variables have been created (the children of the controllers are created together with the object), we need to set the NodeHandleType to ExternalPolled. The method SetVariableConfiguration finds the variable node by using the of the controller and the BrowseName of the child, and sets the NodeHandleType and UserData (in this example system address in the UnderlyingSystem) that can be used for Read and Write.

// create object.
settings = new CreateObjectSettings()
{
ParentNodeId = new NodeId("Controllers", InstanceNamespaceIndex),
ReferenceTypeId = ReferenceTypeIds.Organizes,
RequestedNodeId = new NodeId(block.Name, InstanceNamespaceIndex),
BrowseName = new QualifiedName(block.Name, TypeNamespaceIndex),
TypeDefinitionId = typeDefinitionId
};
CreateObject(Server.DefaultRequestContext, settings);
// Set NodeHandleType to ExternalPolled
foreach (BlockProperty property in block.Properties)
{
// the node was already created when the controller object was instantiated.
// this call links the node to the underlying system data.
VariableNode variable = SetVariableConfiguration(
new NodeId(block.Name, InstanceNamespaceIndex),
new QualifiedName(property.Name, TypeNamespaceIndex),
NodeHandleType.ExternalPolled,
new SystemAddress() { Address = block.Address, Offset = property.Offset });

For polling the variables of the UnderlyingSystem, we have to override the Read method in the NodeManager. The SystemAddress from the last step is added to the NodeHandle and can be used for reading the variables of the UnderlyingSystem. The values have to be added to DataValues and returned to the caller using the callback that is passed by the transaction.

protected override void Read(
RequestContext context,
TransactionHandle transaction,
IList<NodeAttributeOperationHandle> operationHandles,
IList<ReadValueId> settings)
{
for (int ii = 0; ii < operationHandles.Count; ii++)
{
DataValue dv = null;
// the data passed to CreateVariable is returned as the UserData in the handle.
SystemAddress address = operationHandles[ii].NodeHandle.UserData as SystemAddress;
if (address != null)
{
// read the data from the underlying system.
object value = m_system.Read(address.Address, address.Offset);
if (value != null)
{
dv = new DataValue(new Variant(value, null), DateTime.UtcNow);
// apply any index range or encoding.
if (!String.IsNullOrEmpty(settings[ii].IndexRange) || !QualifiedName.IsNull(settings[ii].DataEncoding))
{
dv = ApplyIndexRangeAndEncoding(
operationHandles[ii].NodeHandle,
dv,
settings[ii].IndexRange,
settings[ii].DataEncoding);
}
}
}
// set an error if not found.
if (dv == null)
{
dv = new DataValue(new StatusCode(StatusCodes.BadNodeIdUnknown));
}
// return the data to the caller.
((ReadCompleteEventHandler)transaction.Callback)(
operationHandles[ii],
transaction.CallbackData,
dv,
false);
}
}

Writing a value to the UnderlyingSystem is very similar to reading values from the UnderlyingSystem.

protected override void Write(
RequestContext context,
TransactionHandle transaction,
IList<NodeAttributeOperationHandle> operationHandles,
IList<WriteValue> settings)
{
for (int ii = 0; ii < operationHandles.Count; ii++)
{
StatusCode error = StatusCodes.Good;
// the data passed to CreateVariable is returned as the UserData in the handle.
SystemAddress address = operationHandles[ii].NodeHandle.UserData as SystemAddress;
if (address != null)
{
if (!String.IsNullOrEmpty(settings[ii].IndexRange))
{
error = StatusCodes.BadIndexRangeInvalid;
}
else if (!m_system.Write(address.Address, address.Offset, settings[ii].Value.Value))
{
error = StatusCodes.BadUserAccessDenied;
}
}
else
{
error = StatusCodes.BadNodeIdUnknown;
}
// return the data to the caller.
((WriteCompleteEventHandler)transaction.Callback)(
operationHandles[ii],
transaction.CallbackData,
error,
false);
}
}

Now, the variable Temperature gets its value from the device. We can test this in UaExpert: Drag & drop the variables of a controller to the Data Access View. The Variable value provided by the Read method is shown in the Value column.

serverlesson03_expert.png
Figure 3.5: Testing implementation in UaExpert

In a last step, we add information about the expected range of values for each variable. The OPC UA AnalogItemType contains a property called EURange that provides this information to clients. This property will not change during runtime, so external polling of the range is not needed. We only need to update the value of the in-memory-node.

// Set NodeHandleType to ExternalPolled
foreach (BlockProperty property in block.Properties)
{
// the node was already created when the controller object was instantiated.
// this call links the node to the underlying system data.
VariableNode variable = SetVariableConfiguration(
new NodeId(block.Name, InstanceNamespaceIndex),
new QualifiedName(property.Name, TypeNamespaceIndex),
NodeHandleType.ExternalPolled,
new SystemAddress() { Address = block.Address, Offset = property.Offset });
// Add information about expected range
if (variable != null)
{
// in-memory nodes must be locked before updates.
// reads do not require locks for simple types and references.
// value reads require a lock.
lock (InMemoryNodeLock)
{
variable.AccessLevel = (property.Writeable) ? AccessLevels.CurrentReadOrWrite : AccessLevels.CurrentRead;
}
if (property.Range != null)
{
SetVariableDefaultValue(
variable.NodeId,
new QualifiedName(BrowseNames.EURange),
new Variant(property.Range));
}
}
}