This lesson will show how to provide Methods in the Address Space.
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 beside 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
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 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());
pMethod->setModellingRuleId(OpcUaId_ModellingRule_Mandatory);
addStatus = addNodeAndReference(pControllerType, pMethod, OpcUaId_HasComponent);
UA_ASSERT(addStatus.
isGood());
pMethod->setModellingRuleId(OpcUaId_ModellingRule_Mandatory);
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 :
{
UA_DISABLE_COPY(ControllerObject);
public:
ControllerObject(
NmBuildingAutomation* pNodeManager,
OpcUa_UInt32 deviceAddress,
BaCommunicationInterface *pCommIf);
virtual ~ControllerObject(void);
protected:
OpcUa_UInt32 m_deviceAddress;
BaCommunicationInterface* m_pCommIf;
private:
};
In order to create the corresponding Object components, we add the following local helper variable
ControllerObject::ControllerObject(
NmBuildingAutomation* pNodeManager,
OpcUa_UInt32 deviceAddress,
BaCommunicationInterface *pCommIf)
m_pSharedMutex(NULL),
m_deviceAddress(deviceAddress),
m_pCommIf(pCommIf)
{
BaUserData* pUserData = NULL;
OpcUa_Int16 nsIdx = pNodeManager->getNameSpaceIndex();
...
and the following source code to the constructor of ControllerObject:
...
...
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 corresponding 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 by default on the class that implements the Object, in our case ControllerObject.
In preparation for this, we derive ControllerObject from MethodManager:
#ifndef __CONTROLLEROBJECT_H__
#define __CONTROLLEROBJECT_H__
#include "uaobjecttypes.h"
#include "userdatabase.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);
protected:
OpcUa_UInt32 m_deviceAddress;
private:
};
Then we override UaObject::getMethodManager() in controllerobject.h
class ControllerObject :
{
UA_DISABLE_COPY(ControllerObject);
public:
ControllerObject(
NmBuildingAutomation* pNodeManager,
OpcUa_UInt32 deviceAddress);
virtual ~ControllerObject(void);
protected:
OpcUa_UInt32 m_deviceAddress;
BaCommunicationInterface* m_pCommIf;
private:
};
and add its implementation to the source file:
#include "controllerobject.h"
#include "nmbuildingautomation.h"
#include "buildingautomationtypeids.h"
#include "opcua_analogitemtype.h"
#include "bacommunicationinterface.h"
#include "methodhandleuanode.h"
...
OpcUa_Byte ControllerObject::eventNotifier() const
{
return Ua_EventNotifier_None;
}
{
OpcUa_ReferenceParameter(pMethod);
}
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 the header file controllerobject.h:
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;
BaCommunicationInterface* m_pCommIf;
private:
};
Then we add the first edition of beginCall to the source file controllerobject.cpp as shown below:
OpcUa_UInt32 callbackHandle,
const UaVariantArray& inputArguments)
{
OpcUa_ReferenceParameter(serviceContext);
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
{
ret = call(pMethod, inputArguments, outputArguments, inputArgumentResults, inputArgumentDiag);
}
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 for 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 method call in the callback,
- the MethodHandle provided by the NodeManager::getMethodHandle, and
- the actual input Arguments.
MethodManagerCallback::finishCall finishes the according Method call. It takes
- the handle for the method call provided by beginCall(),
- the result(s) of the actual input Argument(s) provided if the overall result is BadInvalidArgument,
- the diagnostic information related to the result(s) of the actual input Argument(s),
- the actual output Argument(s), and
- the overall result of the Method call 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.
Completing First Edition of beginCall()
For this we replace the marked lines
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
{
}
with the following code
if ( pMethod->
nodeId() == m_pMethodStart->nodeId())
{
if ( inputArguments.length() > 0 )
{
ret = OpcUa_BadInvalidArgument;
}
else
{
ret = m_pCommIf->setControllerState(
m_deviceAddress,
BaCommunicationInterface::Ba_ControllerState_On );
}
}
else if ( pMethod->
nodeId() == m_pMethodStop->nodeId())
{
if ( inputArguments.length() > 0 )
{
ret = OpcUa_BadInvalidArgument;
}
else
{
ret = m_pCommIf->setControllerState(
m_deviceAddress,
BaCommunicationInterface::Ba_ControllerState_Off );
}
Step 4: Calling Stop Method with UA client
To test the method implementation, compile and run the server, then connect to it with UaExpert. The methods Start and Stop will now show up in the server’s Address Space beneath the controller object nodes (see Figure 4-2).
Drag and Drop State from the Address to the Default DA View tab. Note that Value is 1, i.e. the controller is running.
Figure 4-2 Monitoring a Variable using UaExpert
Right-click on the Stop item in the Address Space browser, select Call and call the method using the newly openend dialog window. The Value of State should switch to 0 (see Figure 4-3).
Figure 4-3: Calling the Stop Method
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. Add the marked lines to controllerobject.h:
...
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 a callback handle and an object but takes a pointer to the instance of UaMethod explicitly and returns output arguments, status codes, and diagnostic information.
The implementation in FurnaceControllerType is shown below, acting for both sub classes. Add the marked code to furnacecontrollerobject.h (and airconditionercontrollerobject.h in a similar way):
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& );
...
and the following code snippet to furnacecontrollerobject.cpp,
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;
}
whereas we need two input arguments in AirconditionerControllerObject::call as shown in the following code to be added to airconditionercontroller.cpp:
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;
}
{
}
}
}
else
{
ret = OpcUa_BadMethodInvalid;
}
}
return ret;
}
This Method resembles in some extent to ControllerObject::beginCall(). Note that we still leave StartWithSetPoint functionality unimplemented in order to extend the definition. Add the following code to furnacecontroller.h (and analogous to airconditionercontroller.h)
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:
};
Furthermore, bacommunicationinterface.h has to be included in furnacecontrollerobject.cpp and airconditionercontrollerobject.cpp:
#include "furnacecontrollerobject.h"
#include "buildingautomationtypeids.h"
#include "nmbuildingautomation.h"
#include "opcua_analogitemtype.h"
#include "bacommunicationinterface.h"
...
Create InstanceDeclarations
In order to do this, we add the following code snippet to NmBuildingAutomation::createTypeNode():
UaStatus NmBuildingAutomation::createTypeNodes()
{
...
...
...
UaNodeId(Ba_AirConditionerControllerType_StartWithSetpoint, getNameSpaceIndex()),
"StartWithSetpoint",
getNameSpaceIndex());
pMethod->setModellingRuleId(OpcUaId_ModellingRule_Mandatory);
UA_ASSERT(addStatus.
isGood());
UaNodeId(Ba_AirConditionerControllerType_StartWithSetpoint_In, getNameSpaceIndex()),
Ua_AccessLevel_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 2 when instantiating the Method in the context of AirConditionControllerType. The implementation for FurnaceControllerType is quite similar, however it provides only one Argument that is TemperatureSetPoint:
...
...
UA_ASSERT(addStatus.
isGood());
UaNodeId(Ba_FurnaceControllerType_StartWithSetpoint, getNameSpaceIndex()),
"StartWithSetpoint",
getNameSpaceIndex());
pMethod->setModellingRuleId(OpcUaId_ModellingRule_Mandatory);
addStatus = addNodeAndReference(pFurnaceControllerType, pMethod, OpcUaId_HasComponent);
UA_ASSERT(addStatus.
isGood());
UaNodeId(Ba_FurnaceControllerType_StartWithSetpoint_In, getNameSpaceIndex()),
Ua_AccessLevel_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 also sets 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 UaExpert
Connect to your server with UaExpert and drag and drop the variables HumiditySetpoint, TemperatureSetpoint, and State of one of the AirConditionerControllers to the Default DA View Window. The Value of State is 1, so it’s necessary to call Stop first.
Then call the method StartWithSetpoint with two Arguments (the new temperature and humidity set points). The Value of State should switch to 1, and the Variables HumiditySetpoint and TemperatureSetpoint should now show the new values passed in the method call (see Figure 4-5).
Figure 4-5 UaExpert calling StartWithSetpoint