This lesson will show how to provide Methods in the Address Space.
Content:
Overview
Following our example which has been introduced in Lesson 2: Extending the Address Space with real world data we will add method support to the controller objects in this lesson. Methods will be used here as Start and Stop commands for the controllers. Specialized methods StartWithSetPoint provide a mechanism to start a controller and to pass in the setpoints in one consistent command.
Figure 4-1 shows the methods Start and Stop added to the ControllerType besides the InstanceDeclarations we already created in Lesson 2: Extending the Address Space with real world data. The method StartWithSetPoint is added to the object types FurnaceControllerType and AirConditionerControllerType with different parameters.
Figure 4-1: ControllerType
The steps 1 to 3 will show you how to create Methods generally exemplified by implementing Start and Stop. In step 5 you will learn how to create Methods with Arguments to be passed to.
Step 1: Add Methods to Object Type
Creating the InstanceDeclaration
First of all we create the according InstanceDeclaration nodes for the ControllerType by adding the methods Start and Stop as components to the object type. This is done in the method NmBuildingAutomation::createTypeNodes. We need additional local helper variables to create the method nodes.
UaStatus NmBuildingAutomation::createTypeNodes()
{
UaUInt32Array nullarray;
After the code for the creation of the ControllerType and its instance declaration variables we are creating the two method nodes with the following code:
UaNodeId(Ba_ControllerType_PowerConsumption, getNameSpaceIndex()),
"PowerConsumption",
getNameSpaceIndex(),
defaultValue,
Ua_AccessLevel_CurrentRead,
this);
addStatus = addNodeAndReference(pControllerType, pDataItem, OpcUaId_HasComponent);
UA_ASSERT(addStatus.
isGood());
addStatus = addNodeAndReference(pControllerType, pMethod, OpcUaId_HasComponent);
UA_ASSERT(addStatus.
isGood());
addStatus = addNodeAndReference(pControllerType, pMethod, OpcUaId_HasComponent);
UA_ASSERT(addStatus.
isGood());
Step 2: Add Methods to Object Instances
Continuing the implementation of ControllerObject::ControllerObject
In a first step we are adding two member variables for the method nodes to the class ControllerObject.
class ControllerObject :
{
private:
};
In order to create the according Object components we add the following local helper variables
OpcUa_Int16 nsIdx = pNodeManager->getNameSpaceIndex();
and the following source code to the constructor of ControllerObject:
pInstanceDeclaration = pNodeManager->getInstanceDeclarationVariable(Ba_ControllerType_PowerConsumption);
UA_ASSERT(pInstanceDeclaration!=NULL);
addStatus = pNodeManager->addNodeAndReference(this, pDataItem, OpcUaId_HasComponent);
UA_ASSERT(addStatus.
isGood());
pUserData = new BaUserData(OpcUa_False, deviceAddress, 2);
addStatus = pNodeManager->addNodeAndReference(this, m_pMethodStart, OpcUaId_HasComponent);
UA_ASSERT(addStatus.
isGood());
sName = "Stop";
addStatus = pNodeManager->addNodeAndReference(this, m_pMethodStop, OpcUaId_HasComponent);
UA_ASSERT(addStatus.
isGood());
}
Step 3: Implementing the handling of the MethodManager
In general the call of a method is based on the NodeId of the particular Object and the NodeId of the according Method.
getMethodHandle()
NodeManagerUaNode, a base class of NmBuildingAutomation implements NodeManager::getMethodHandle() which takes exactly these NodeIds to find the object that implements the MethodManager for the method to call
Provide MethodManager
In addition we have to implement the interface MethodManager. The implementation is on the class that implements the Object, in our case ControllerObject, by default.
In preparation for this we derive ControllerObject from MethodManager:
#ifndef __CONTROLLEROBJECT_H__
#define __CONTROLLEROBJECT_H__
#include "uaobjecttypes.h"
#include "methodmanager.h"
class NmBuildingAutomation;
class ControllerObject :
{
UA_DISABLE_COPY(ControllerObject);
public:
ControllerObject(
NmBuildingAutomation* pNodeManager,
OpcUa_UInt32 deviceAddress);
virtual ~ControllerObject(void);
protected:
OpcUa_UInt32 m_deviceAddress;
private:
};
Then we override UaObject::getMethodManager()
#ifndef __CONTROLLEROBJECT_H__
#define __CONTROLLEROBJECT_H__
#include "uaobjecttypes.h"
#include "methodmanager.h"
class NmBuildingAutomation;
class ControllerObject :
{
UA_DISABLE_COPY(ControllerObject);
public:
ControllerObject(
NmBuildingAutomation* pNodeManager,
OpcUa_UInt32 deviceAddress);
virtual ~ControllerObject(void);
protected:
OpcUa_UInt32 m_deviceAddress;
private:
};
#endif
and add its implementation to source file:
...
ControllerObject::~ControllerObject(void)
{
}
OpcUa_Byte ControllerObject::eventNotifier() const
{
return Ua_EventNotifier_None;
}
{
}
This method will be invoked if a Method belonging to ControllerObject is called. Because the latter is derived from MethodManager we just need to return this casted to the MethodManager interface.
Method invocation
In order to implement the MethodManager interface we add beginCall() to header:
@code
#ifndef __CONTROLLEROBJECT_H__
#define __CONTROLLEROBJECT_H__
#include "uaobjecttypes.h"
#include "methodmanager.h"
class NmBuildingAutomation;
class ControllerObject :
{
UA_DISABLE_COPY(ControllerObject);
public:
ControllerObject(
NmBuildingAutomation* pNodeManager,
OpcUa_UInt32 deviceAddress);
virtual ~ControllerObject(void);
OpcUa_UInt32 callbackHandle,
const UaVariantArray& inputArguments);
protected:
OpcUa_UInt32 m_deviceAddress;
private:
};
#endif
Then we add the first edition of beginCall to source file as shown below:
OpcUa_UInt32 callbackHandle,
const UaVariantArray& inputArguments)
{
UaVariantArray outputArguments;
UaStatusCodeArray inputArgumentResults;
UaDiagnosticInfos inputArgumentDiag;
if(pMethodHandleUaNode)
{
if(pMethod)
{
if ( pMethod->
nodeId() == m_pMethodStart->nodeId() )
{
if ( inputArguments.length() > 0 )
{
ret = OpcUa_BadInvalidArgument;
}
else
{
}
}
else if ( pMethod->
nodeId() == m_pMethodStop->nodeId())
{
if ( inputArguments.length() > 0 )
{
ret = OpcUa_BadInvalidArgument;
}
else
{
}
}
else
{
assert(false);
ret = OpcUa_BadInvalidArgument;
}
callbackHandle,
inputArgumentResults,
inputArgumentDiag,
outputArguments,
ret);
ret = OpcUa_Good;
}
else
{
assert(false);
ret = OpcUa_BadInvalidArgument;
}
}
else
{
assert(false);
ret = OpcUa_BadInvalidArgument;
}
return ret;
}
beginCall() calls a particular Method of an UA Object. It takes
- a callback interface used by the IOManager to finish the action for each Node passed in the transaction,
- a general context for the service calls containing information like the session object, return diagnostic mask, and timeout hint,
- the handle for the Node in the callback. It has been passed to the IOManager with the beginModifyMonitoring method,
- a handle for the Method Node, and
- the actual input Arguments.
MethodManagerCallback::finishCall finishes the according Method call. It takes
- the callback interface,
- the result(s) of the actual input Argument(s),
- the actual output Argument(s), and
- the result of the StopMonitoring operation.
This version of beginCall() only allows ControllerObject::Start(). Because Start() is not taking any arguments beginCall() checks if the number of arguments equals zero. Before we are going to implement the method call in itself we have to introduce BaCommunicationInterface.
Introducing BaCommunicationInterface
We extend the definition of ControllerObject as shown above:
#ifndef __CONTROLLEROBJECT_H__
#define __CONTROLLEROBJECT_H__
#include "uaobjecttypes.h"
#include "methodmanager.h"
class NmBuildingAutomation;
class BaCommunicationInterface;
class ControllerObject :
{
UA_DISABLE_COPY(ControllerObject);
public:
ControllerObject(
NmBuildingAutomation* pNodeManager,
OpcUa_UInt32 deviceAddress,
BaCommunicationInterface *pCommIf);
virtual ~ControllerObject(void);
OpcUa_UInt32 callbackHandle,
const UaVariantArray& inputArguments);
protected:
OpcUa_UInt32 m_deviceAddress;
BaCommunicationInterface *m_pCommIf;
private:
};
#endif
And add to source file:
#include "controllerobject.h"
#include "nmbuildingautomation.h"
#include "uadatavariablecache.h"
#include "bacontrollervariable.h"
#include "bacommunicationinterface.h"
ControllerObject::ControllerObject(
NmBuildingAutomation* pNodeManager,
OpcUa_UInt32 deviceAddress,
BaCommunicationInterface *pCommIf)
m_pSharedMutex(NULL),
m_deviceAddress(deviceAddress),
m_pCommIf(pCommIf)
{
Completing first edition of beginCall()
For this we replace
if ( pMethod->
nodeId() == m_pMethodStart->nodeId() )
{
if ( inputArguments.length() > 0 )
{
ret = OpcUa_BadInvalidArgument;
}
else
{
}
}
by
if ( pMethod->
nodeId() == m_pMethodStart->nodeId())
{
if ( inputArguments.length() > 0 )
{
ret = OpcUa_BadInvalidArgument;
}
else
{
ret = m_pCommIf->setControllerState(
m_deviceAddress,
BaCommunicationInterface::Ba_ControllerState_On );
}
}
Step 4: Calling Start Method with UA client
It is time to test Start with a client.
Monitor State
Figure 4-2 uses UA Expert for this:
Figure 4-2: Monitoring a Variable using UA Expert
Drag and Drop State into the Default DA View tab. Note that Value is 0. Right-click the Start item in the Address Space browser and click Call. We do not have to enter any arguments:
Figure 4-3: Calling the Start Method
Value has switched to 1.
Step 5: Creating a Method having Arguments
Finally we are going to create StartWithSetPoint, an InstanceDeclaration of the two subtypes of ControllerType as Figure 4-4 indicates:
Figure 4-4: ControllerType subtypes
We will implement this Method as we did above. However this Method takes Argument(s), unlike Start and Stop do.
As demonstrated in figure 4-4 AirConditionerControllerType has InstanceDeclaration HumiditySetPoint in addition to InstanceDeclaration TemperatureSetPoint derived from ControllerType. FurnaceControllerType, however, only derives TemperatureSetPoint. Due to this we will implement the Method depending on the controller type.
Preparations
For this reason we introduce a particular Method to ControllerObject, call(), to be overwritten in the sub classes:
...
OpcUa_UInt32 callbackHandle,
const UaVariantArray& inputArguments);
const UaVariantArray& ,
UaVariantArray& ,
UaStatusCodeArray& ,
UaDiagnosticInfos& ) { return OpcUa_BadMethodInvalid; }
...
Unlike beginCall() this Method is called synchronously. That is call() does not ask for callback handle and object but takes a pointer to UaMethod instance explicitly and returns output arguments, status codes, and diagnostic information.
The implementation in FurnaceControllerType is shown below, acting for both sub classes:
virtual UaNodeId typeDefinitionId()
const;
const UaVariantArray& inputArguments,
UaVariantArray& ,
UaStatusCodeArray& inputArgumentResults,
UaDiagnosticInfos& );
and:
const UaVariantArray& inputArguments,
UaVariantArray& ,
UaStatusCodeArray& inputArgumentResults,
UaDiagnosticInfos& )
{
if(pMethod)
{
if ( pMethod->
nodeId() == m_pMethodStartWithSetpoint->nodeId())
{
if ( inputArguments.length() != 1 )
{
ret = OpcUa_BadInvalidArgument;
}
else
{
inputArgumentResults.create(1);
if ( inputArguments[0].Datatype != OpcUaType_Double )
{
ret = OpcUa_BadInvalidArgument;
inputArgumentResults[0] = OpcUa_BadTypeMismatch;
}
else
{
}
}
}
else
{
ret = OpcUa_BadMethodInvalid;
}
}
return ret;
}
This Method resembles in some extend to ControllerObject::beginCall(). Note that we still leave StartWithSetPoint functionality unimplemented in order to extend the definition:
class FurnaceControllerObject :
public ControllerObject
{
public:
FurnaceControllerObject(
NmBuildingAutomation* pNodeManager,
OpcUa_UInt32 deviceAddress,
BaCommunicationInterface *pCommIf
);
virtual ~FurnaceControllerObject(void);
virtual UaNodeId typeDefinitionId()
const;
const UaVariantArray& inputArguments,
UaVariantArray& ,
UaStatusCodeArray& inputArgumentResults,
UaDiagnosticInfos& );
private:
};
and the implementation:
#include "furnacecontrollerobject.h"
#include "buildingautomationtypeids.h"
#include "nmbuildingautomation.h"
#include "bacontrollervariable.h"
#include "bacommunicationinterface.h"
FurnaceControllerObject::FurnaceControllerObject(
NmBuildingAutomation* pNodeManager,
OpcUa_UInt32 deviceAddress,
BaCommunicationInterface *pCommIf
)
: ControllerObject(name, newNodeId, defaultLocaleId, pNodeManager, deviceAddress , pCommIf )
{
...
of the two sub classes as shown above in examplary manner.
Create InstanceDeclarations
In order to do this we add the following code snippet to NmBuildingAutomation::createTypeNode():
...
UaPropertyMethodArgument* pPropertyArg = NULL;
UaUInt32Array nullarray;
...
...
"StartWithSetpoint",
UaNodeId(Ba_AirConditionerControllerType_StartWithSetpoint, getNameSpaceIndex()),
m_defaultLocaleId);
addStatus = addNodeAndReference(pAirConditionerControllerType, pMethod, OpcUaId_HasComponent);
UA_ASSERT(addStatus.
isGood());
UaNodeId(Ba_AirConditionerControllerType_StartWithSetpoint_In, getNameSpaceIndex()),
OpcUa_AccessLevels_CurrentRead,
2,
pPropertyArg->setArgument(
0,
"TemperatureSetPoint",
-1,
nullarray,
pPropertyArg->setArgument(
1,
"HumiditySetpoint",
-1,
nullarray,
addStatus = addNodeAndReference(pMethod, pPropertyArg, OpcUaId_HasProperty);
UA_ASSERT(addStatus.
isGood());
...
Note that we have to set the number of Arguments to two when instantiating the Method in AirConditionControllerType context. The implementation for FurnaceControllerType is quite similar, however it provides only one Argument that is TemperatureSetPoint:
...
...
"StartWithSetpoint",
UaNodeId(Ba_FurnaceControllerType_StartWithSetpoint, getNameSpaceIndex()),
m_defaultLocaleId);
addStatus = addNodeAndReference(pFurnaceControllerType, pMethod, OpcUaId_HasComponent);
UA_ASSERT(addStatus.
isGood());
UaNodeId(Ba_FurnaceControllerType_StartWithSetpoint_In, getNameSpaceIndex()),
OpcUa_AccessLevels_CurrentRead,
1,
pPropertyArg->setArgument(
0,
"TemperatureSetPoint",
-1,
nullarray,
addStatus = addNodeAndReference(pMethod, pPropertyArg, OpcUaId_HasProperty);
UA_ASSERT(addStatus.
isGood());
...
Create methods as components of object instances
In order to do this we add the following code snippet to FurnaceControllerObject::FurnaceControllerObject
UaUInt32Array nullarray;
OpcUa_Int16 nsIdx = pNodeManager->getNameSpaceIndex();
sName,
m_defaultLocaleId);
addStatus = pNodeManager->addNodeAndReference(this, m_pMethodStartWithSetpoint, OpcUaId_HasComponent);
UA_ASSERT(addStatus.
isGood());
sName = "StartWithSetpoint";
sNodeId =
UaString(
"%1.%2").
arg(m_pMethodStartWithSetpoint->nodeId().toString()).arg(sName);
Ua_AccessLevel_CurrentRead,
1,
0,
"TemperatureSetPoint",
OpcUa_ValueRanks_Scalar,
nullarray,
addStatus = pNodeManager->addNodeAndReference(m_pMethodStartWithSetpoint, pPropertyArg, OpcUaId_HasProperty);
UA_ASSERT(addStatus.
isGood());
}
and to AirConditionerControllerObject::AirConditionerControllerObject:
UaUInt32Array nullarray;
OpcUa_Int16 nsIdx = pNodeManager->getNameSpaceIndex();
sName,
m_defaultLocaleId);
addStatus = pNodeManager->addNodeAndReference(this, m_pMethodStartWithSetpoint, OpcUaId_HasComponent);
UA_ASSERT(addStatus.
isGood());
sName = "StartWithSetpoint";
sNodeId =
UaString(
"%1.%2").
arg(m_pMethodStartWithSetpoint->nodeId().toString()).arg(sName);
Ua_AccessLevel_CurrentRead,
2,
0,
"TemperatureSetPoint",
OpcUa_ValueRanks_Scalar,
nullarray,
1,
"HumiditySetpoint",
OpcUa_ValueRanks_Scalar,
nullarray,
addStatus = pNodeManager->addNodeAndReference(m_pMethodStartWithSetpoint, pPropertyArg, OpcUaId_HasProperty);
UA_ASSERT(addStatus.
isGood());
}
Complete call() implementation in sub classes
Now we finish this lesson in completing call() as shown below for FurnaceControllerObject.
const UaVariantArray& inputArguments,
UaVariantArray& ,
UaStatusCodeArray& inputArgumentResults,
UaDiagnosticInfos& )
{
if(pMethod)
{
if ( pMethod->
nodeId() == m_pMethodStartWithSetpoint->nodeId())
{
if ( inputArguments.length() != 1 )
{
ret = OpcUa_BadInvalidArgument;
}
else
{
inputArgumentResults.create(1);
if ( inputArguments[0].Datatype != OpcUaType_Double )
{
ret = OpcUa_BadInvalidArgument;
inputArgumentResults[0] = OpcUa_BadTypeMismatch;
}
else
{
OpcUa_Double value;
ret = m_pCommIf->setControllerState(
m_deviceAddress,
BaCommunicationInterface::Ba_ControllerState_On );
{
vTemp = inputArguments[0];
ret = m_pCommIf->setControllerData(
m_deviceAddress,
1,
value);
}
}
}
}
else
{
ret = OpcUa_BadMethodInvalid;
}
}
return ret;
}
In addition to the state flag this implementation sets also the setpoint for temperature. The code below demonstrates that for AirConditionControllerObject whereas two setpoints are set:
UaStatus AirConditionerControllerObject::call(
const UaVariantArray& inputArguments,
UaVariantArray& ,
UaStatusCodeArray& inputArgumentResults,
UaDiagnosticInfos& )
{
if(pMethod)
{
if ( pMethod->
nodeId() == m_pMethodStartWithSetpoint->nodeId())
{
if ( inputArguments.length() != 2 )
{
ret = OpcUa_BadInvalidArgument;
}
else
{
inputArgumentResults.create(2);
if ( inputArguments[0].Datatype != OpcUaType_Double )
{
ret = OpcUa_BadInvalidArgument;
inputArgumentResults[0] = OpcUa_BadTypeMismatch;
}
if ( inputArguments[1].Datatype != OpcUaType_Double )
{
ret = OpcUa_BadInvalidArgument;
inputArgumentResults[1] = OpcUa_BadTypeMismatch;
}
{
OpcUa_Double value;
ret = m_pCommIf->setControllerState(
m_deviceAddress,
BaCommunicationInterface::Ba_ControllerState_On );
{
vTemp = inputArguments[0];
ret = m_pCommIf->setControllerData(
m_deviceAddress,
1,
value);
vTemp = inputArguments[1];
ret = m_pCommIf->setControllerData(
m_deviceAddress,
4,
value);
}
}
}
}
else
{
ret = OpcUa_BadMethodInvalid;
}
}
return ret;
}
Call StartWithSetpoint with UA Expert
Figure 4-5 demonstrates how to call StartWithSetpoint and passing two arguments to it with UA Expert:
Figure 4-5: UA Expert calling StartWithSetpoint