C++ Based OPC UA Client/Server SDK  1.5.5.355
Lesson 4: Adding Support for Methods

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

l4gettingstartedlesson04_controller_objecttypeall.png

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()
{
UaStatus ret;
UaStatus addStatus;
UaVariant defaultValue;
UaObjectTypeSimple* pControllerType = NULL;
UaObjectTypeSimple* pAirConditionerControllerType = NULL;
UaObjectTypeSimple* pFurnaceControllerType = NULL;
OpcUa::DataItemType* pDataItem; OpcUa::AnalogItemType* pAnalogItem;
// Method helpers, add the following lines
OpcUa::BaseMethod* pMethod = NULL;
UaPropertyMethodArgument* pPropertyArg = NULL;
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:

...
// Add Variable "PowerConsumption"
defaultValue.setDouble(0);
pDataItem = new OpcUa::DataItemType(
UaNodeId(Ba_ControllerType_PowerConsumption, getNameSpaceIndex()),
"PowerConsumption",
getNameSpaceIndex(),
defaultValue,
Ua_AccessLevel_CurrentRead,
this);
pDataItem->setModellingRuleId(OpcUaId_ModellingRule_Mandatory);
addStatus = addNodeAndReference(pControllerType, pDataItem, OpcUaId_HasComponent);
UA_ASSERT(addStatus.isGood());
// New code begins
// Add Method "Start"
pMethod = new OpcUa::BaseMethod(UaNodeId(Ba_ControllerType_Start, getNameSpaceIndex()), "Start", getNameSpaceIndex());
pMethod->setModellingRuleId(OpcUaId_ModellingRule_Mandatory);
addStatus = addNodeAndReference(pControllerType, pMethod, OpcUaId_HasComponent);
UA_ASSERT(addStatus.isGood());
// Add Method "Stop"
pMethod = new OpcUa::BaseMethod(UaNodeId(Ba_ControllerType_Stop, getNameSpaceIndex()), "Stop", getNameSpaceIndex());
pMethod->setModellingRuleId(OpcUaId_ModellingRule_Mandatory);
addStatus = addNodeAndReference(pControllerType, pMethod, OpcUaId_HasComponent);
UA_ASSERT(addStatus.isGood());
// New code ends
/**************************************************************
* Create the AirConditionerController Type
*/

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 :
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;
BaCommunicationInterface* m_pCommIf;
// New code begin
private:
UaMethodGeneric* m_pMethodStart;
UaMethodGeneric* m_pMethodStop;
// New code end
};

In order to create the corresponding Object components, we add the following local helper variable

ControllerObject::ControllerObject(
const UaString& name,
const UaNodeId& newNodeId,
const UaString& defaultLocaleId,
NmBuildingAutomation* pNodeManager,
OpcUa_UInt32 deviceAddress,
BaCommunicationInterface *pCommIf)
: UaObjectBase(name, newNodeId, defaultLocaleId),
m_pSharedMutex(NULL),
m_deviceAddress(deviceAddress),
m_pCommIf(pCommIf)
{
// Use a mutex shared across all variables of this object
m_pSharedMutex = new UaMutexRefCounted;
UaVariable* pInstanceDeclaration = NULL;
OpcUa::BaseDataVariableType* pDataVariable = NULL;
OpcUa::DataItemType* pDataItem = NULL;
OpcUa::AnalogItemType* pAnalogItem = NULL;
BaUserData* pUserData = NULL;
UaStatus addStatus;
// Method helper, add this line
OpcUa_Int16 nsIdx = pNodeManager->getNameSpaceIndex();
...

and the following source code to the constructor of ControllerObject:

...
// Add Variable "PowerConsumption"
...
// Change value handling to get read and write calls to the node manager
// New code begins
// Add Method "Start"
UaString sName = "Start";
UaString sNodeId = UaString("%1.%2").arg(newNodeId.toString()).arg(sName);
m_pMethodStart = new UaMethodGeneric(sName, UaNodeId(sNodeId, nsIdx), m_defaultLocaleId);
addStatus = pNodeManager->addNodeAndReference(this, m_pMethodStart, OpcUaId_HasComponent);
UA_ASSERT(addStatus.isGood());
// Add Method "Stop"
sName = "Stop";
sNodeId = UaString("%1.%2").arg(newNodeId.toString()).arg(sName);
m_pMethodStop = new UaMethodGeneric(sName, UaNodeId(sNodeId, nsIdx), m_defaultLocaleId);
addStatus = pNodeManager->addNodeAndReference(this, m_pMethodStop, OpcUaId_HasComponent);
UA_ASSERT(addStatus.isGood());
// New code ends
}

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" // Add this line
class NmBuildingAutomation;
class BaCommunicationInterface;
class UaMethodGeneric; // Add this line
class ControllerObject :
public UaObjectBase, // Line has changed
public MethodManager // Add this line
{
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:
OpcUa_UInt32 m_deviceAddress;
private:
UaMethodGeneric* m_pMethodStart;
UaMethodGeneric* m_pMethodStop;
};

Then we override UaObject::getMethodManager() in controllerobject.h

class ControllerObject :
public UaObjectBase,
{
UA_DISABLE_COPY(ControllerObject);
public:
ControllerObject(
const UaString& name,
const UaNodeId& newNodeId,
const UaString& defaultLocaleId,
NmBuildingAutomation* pNodeManager,
OpcUa_UInt32 deviceAddress);
virtual ~ControllerObject(void);
OpcUa_Byte eventNotifier() const;
// Override UaObject method implementation
MethodManager* getMethodManager(UaMethod* pMethod) const; // Add this line
protected:
UaMutexRefCounted* m_pSharedMutex;
OpcUa_UInt32 m_deviceAddress;
BaCommunicationInterface* m_pCommIf;
private:
UaMethodGeneric* m_pMethodStart;
UaMethodGeneric* m_pMethodStop;
};

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" // Add this line
...
OpcUa_Byte ControllerObject::eventNotifier() const
{
return Ua_EventNotifier_None;
}
// New code begins
MethodManager* ControllerObject::getMethodManager(UaMethod* pMethod) const
{
OpcUa_ReferenceParameter(pMethod);
return (MethodManager*)this;
}
// New code ends

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 :
public UaObjectBase,
{
UA_DISABLE_COPY(ControllerObject);
public:
ControllerObject(
const UaString& name,
const UaNodeId& newNodeId,
const UaString& defaultLocaleId,
NmBuildingAutomation* pNodeManager,
OpcUa_UInt32 deviceAddress);
virtual ~ControllerObject(void);
OpcUa_Byte eventNotifier() const;
// Override UaObject method implementation
// New code begins
// Implement MethodManager interface
const ServiceContext& serviceContext,
OpcUa_UInt32 callbackHandle,
MethodHandle* pMethodHandle,
const UaVariantArray& inputArguments);
// New code ends
protected:
UaMutexRefCounted* m_pSharedMutex;
OpcUa_UInt32 m_deviceAddress;
BaCommunicationInterface* m_pCommIf;
private:
UaMethodGeneric* m_pMethodStart;
UaMethodGeneric* m_pMethodStop;
};

Then we add the first edition of beginCall to the source file controllerobject.cpp as shown below:

UaStatus ControllerObject::beginCall(
MethodManagerCallback* pCallback, // Callback interface for the transaction
const ServiceContext& serviceContext, // Context for the service calls
OpcUa_UInt32 callbackHandle, // Handle for the Node in the callback
MethodHandle* pMethodHandle, // Handle for the Node of the Method
const UaVariantArray& inputArguments) // Actual input Argument(s)
{
OpcUa_ReferenceParameter(serviceContext);
UaStatus ret;
UaVariantArray outputArguments;
UaStatusCodeArray inputArgumentResults;
UaDiagnosticInfos inputArgumentDiag;
MethodHandleUaNode* pMethodHandleUaNode = static_cast<MethodHandleUaNode*>(pMethodHandle);
UaMethod* pMethod = NULL;
if(pMethodHandleUaNode)
{
pMethod = pMethodHandleUaNode->pUaMethod();
if(pMethod)
{
// Check if we have the start method
if ( pMethod->nodeId() == m_pMethodStart->nodeId() )
{
// Number of input arguments must be 0
if ( inputArguments.length() > 0 )
{
ret = OpcUa_BadInvalidArgument;
}
else
{
// To be done: the method call itself
}
}
// Check if we have the stop method
else if ( pMethod->nodeId() == m_pMethodStop->nodeId())
{
// Number of input arguments must be 0
if ( inputArguments.length() > 0 )
{
ret = OpcUa_BadInvalidArgument;
}
else
{
// To be done: the method call itself
}
}
else
{
ret = call(pMethod, inputArguments, outputArguments, inputArgumentResults, inputArgumentDiag);
}
pCallback->finishCall(
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

// Check if we have the start method
if ( pMethod->nodeId() == m_pMethodStart->nodeId() )
{
// Number of input arguments must be 0
if ( inputArguments.length() > 0 )
{
ret = OpcUa_BadInvalidArgument;
}
else
{
// Remove the following line
// To be done: the method call itself
}
}
// Check if we have the stop method
else if ( pMethod->nodeId() == m_pMethodStop->nodeId())
{
// Number of input arguments must be 0
if ( inputArguments.length() > 0 )
{
ret = OpcUa_BadInvalidArgument;
}
else
{
// Remove the following line
// To be done: the method call itself
}

with the following code

// Check if we have the start method
if ( pMethod->nodeId() == m_pMethodStart->nodeId())
{
// Number of input arguments must be 0
if ( inputArguments.length() > 0 )
{
ret = OpcUa_BadInvalidArgument;
}
else
{
// New code begins
ret = m_pCommIf->setControllerState(
m_deviceAddress,
BaCommunicationInterface::Ba_ControllerState_On );
// New code ends
}
}
// Check if we have the stop method
else if ( pMethod->nodeId() == m_pMethodStop->nodeId())
{
// Number of input arguments must be 0
if ( inputArguments.length() > 0 )
{
ret = OpcUa_BadInvalidArgument;
}
else
{
// New code begins
ret = m_pCommIf->setControllerState(
m_deviceAddress,
BaCommunicationInterface::Ba_ControllerState_Off );
// New code ends
}

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

l4gettingstartedlesson04_monitor_state_with_uaexpert.png

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

l4gettingstartedlesson04_call_start_with_uaexpert.png

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

l4gettingstartedlesson04_controllertypes_with_startwithsetpoint.png

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:

...
// Implement MethodManager interface
virtual UaStatus beginCall(
const ServiceContext& serviceContext,
OpcUa_UInt32 callbackHandle,
MethodHandle* pMethodHandle,
const UaVariantArray& inputArguments);
// New code begins
// Own synchronous call implementation that can be overridden in subclasses
virtual UaStatus call(
UaMethod* /*pMethod*/,
const UaVariantArray& /*inputArguments*/,
UaVariantArray& /*outputArguments*/,
UaStatusCodeArray& /*inputArgumentResults*/,
UaDiagnosticInfos& /*inputArgumentDiag*/) { return OpcUa_BadMethodInvalid; }
// New code ends
...

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(
const UaString& name,
const UaNodeId& newNodeId,
const UaString& defaultLocaleId,
NmBuildingAutomation* pNodeManager,
OpcUa_UInt32 deviceAddress,
BaCommunicationInterface *pCommIf);
virtual ~FurnaceControllerObject(void);
virtual UaNodeId typeDefinitionId() const;
// New code begins
// override Controller::call()
virtual UaStatus call(
UaMethod* pMethod,
const UaVariantArray& inputArguments,
UaVariantArray& /*outputArguments*/,
UaStatusCodeArray& inputArgumentResults,
UaDiagnosticInfos& /*inputArgumentDiag*/);
// New code ends
...

and the following code snippet to furnacecontrollerobject.cpp,

UaStatus FurnaceControllerObject::call(
UaMethod* pMethod,
const UaVariantArray& inputArguments,
UaVariantArray& /*outputArguments*/,
UaStatusCodeArray& inputArgumentResults,
UaDiagnosticInfos& /*inputArgumentDiag*/)
{
UaStatus ret;
if(pMethod)
{
// Check if we have the start method
if ( pMethod->nodeId() == m_pMethodStartWithSetpoint->nodeId())
{
// Number of input arguments must be 1
if ( inputArguments.length() != 1 )
{
ret = OpcUa_BadInvalidArgument;
}
else
{
inputArgumentResults.create(1);
// validate input parameter
if ( inputArguments[0].Datatype != OpcUaType_Double )
{
ret = OpcUa_BadInvalidArgument;
inputArgumentResults[0] = OpcUa_BadTypeMismatch;
}
else
{
// To be done
}
}
}
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(
UaMethod* pMethod,
const UaVariantArray& inputArguments,
UaVariantArray& /*outputArguments*/,
UaStatusCodeArray& inputArgumentResults,
UaDiagnosticInfos& /*inputArgumentDiag*/)
{
UaStatus ret;
if (pMethod)
{
// Check if we have the start method
if ( pMethod->nodeId() == m_pMethodStartWithSetpoint->nodeId())
{
// Number of input arguments must be 2
if ( inputArguments.length() != 2 )
{
ret = OpcUa_BadInvalidArgument;
}
else
{
inputArgumentResults.create(2);
// validate input parameters
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;
}
if (ret.isGood())
{
// To be done
}
}
}
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(
const UaString& name,
const UaNodeId& newNodeId,
const UaString& defaultLocaleId,
NmBuildingAutomation* pNodeManager,
OpcUa_UInt32 deviceAddress,
BaCommunicationInterface *pCommIf
);
virtual ~FurnaceControllerObject(void);
virtual UaNodeId typeDefinitionId() const;
// override Controller::call()
virtual UaStatus call(
UaMethod* pMethod,
const UaVariantArray& inputArguments,
UaVariantArray& /*outputArguments*/,
UaStatusCodeArray& inputArgumentResults,
UaDiagnosticInfos& /*inputArgumentDiag*/);
// New code begin
private:
UaMethodGeneric *m_pMethodStartWithSetpoint;
// New code end
};

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" // Add this line
...

Create InstanceDeclarations

In order to do this, we add the following code snippet to NmBuildingAutomation::createTypeNode():

UaStatus NmBuildingAutomation::createTypeNodes()
{
...
/***************************************************************
* Create the AirConditionerController Type Instance declaration
*/
...
// Add Variable "HumiditySetpoint"
...
pAnalogItem->setEngineeringUnits(tempEUPercent);
// New code begins
// Add Method "StartWithSetpoint"
pMethod = new OpcUa::BaseMethod(
UaNodeId(Ba_AirConditionerControllerType_StartWithSetpoint, getNameSpaceIndex()),
"StartWithSetpoint",
getNameSpaceIndex());
pMethod->setModellingRuleId(OpcUaId_ModellingRule_Mandatory);
UA_ASSERT(addStatus.isGood());
// Add Property "InputArguments"
pPropertyArg = new UaPropertyMethodArgument(
UaNodeId(Ba_AirConditionerControllerType_StartWithSetpoint_In, getNameSpaceIndex()), // NodeId of the property
Ua_AccessLevel_CurrentRead, // Access level of the property
2, // Number of arguments
// Argument TemperatureSetPoint
pPropertyArg->setArgument(
0, // Index of the argument
"TemperatureSetPoint", // Name of the argument
UaNodeId(OpcUaId_Double),// Data type of the argument
-1, // Array rank of the argument
nullarray, // Array dimensions of the argument
UaLocalizedText("en", "Controller set point for temperature")); // Description
// Argument HumiditySetpoint
pPropertyArg->setArgument(
1, // Index of the argument
"HumiditySetpoint", // Name of the argument
UaNodeId(OpcUaId_Double),// Data type of the argument
-1, // Array rank of the argument
nullarray, // Array dimensions of the argument
UaLocalizedText("en", "Controller set point for humidity")); // Description
// Add property to method
addStatus = addNodeAndReference(pMethod, pPropertyArg, OpcUaId_HasProperty);
UA_ASSERT(addStatus.isGood());
// New code ends
...

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:

...
/**************************************************************
* Create the FurnaceController Type Instance declaration
*/
// Add Variable "GasFlow"
...
UA_ASSERT(addStatus.isGood());
// New code begins
// Add Method "StartWithSetpoint"
pMethod = new OpcUa::BaseMethod(
UaNodeId(Ba_FurnaceControllerType_StartWithSetpoint, getNameSpaceIndex()),
"StartWithSetpoint",
getNameSpaceIndex());
pMethod->setModellingRuleId(OpcUaId_ModellingRule_Mandatory);
addStatus = addNodeAndReference(pFurnaceControllerType, pMethod, OpcUaId_HasComponent);
UA_ASSERT(addStatus.isGood());
// Add Property "InputArguments"
pPropertyArg = new UaPropertyMethodArgument(
UaNodeId(Ba_FurnaceControllerType_StartWithSetpoint_In, getNameSpaceIndex()), // NodeId of the property
Ua_AccessLevel_CurrentRead, // Access level of the property
1, // Number of arguments
// Argument TemperatureSetPoint
pPropertyArg->setArgument(
0, // Index of the argument
"TemperatureSetPoint", // Name of the argument
UaNodeId(OpcUaId_Double),// Data type of the argument
-1, // Array rank of the argument
nullarray, // Array dimensions of the argument
UaLocalizedText("en", "Controller set point for temperature")); // Description
// Add property to method
addStatus = addNodeAndReference(pMethod, pPropertyArg, OpcUaId_HasProperty);
UA_ASSERT(addStatus.isGood());
// New code ends
...

Create Methods as Components of Object Instances

In order to do this, we add the following code snippet to FurnaceControllerObject::FurnaceControllerObject

// FurnaceControllerObject::FurnaceControllerObject
...
UaStatus addStatus;
// New code begins
// Method helpers
UaPropertyMethodArgument* pPropertyArg = NULL;
UaUInt32Array nullarray;
OpcUa_Int16 nsIdx = pNodeManager->getNameSpaceIndex();
// New code ends
/**************************************************************
* Create the FurnaceController components
*/
...
// Change value handling to get read and write calls to the node manager
// New code begins
// Add Method "StartWithSetpoint"
UaString sName = "StartWithSetpoint";
UaString sNodeId = UaString("%1.%2").arg(newNodeId.toString()).arg(sName);
m_pMethodStartWithSetpoint = new UaMethodGeneric(
sName,
UaNodeId(sNodeId, nsIdx),
m_defaultLocaleId);
addStatus = pNodeManager->addNodeAndReference(this, m_pMethodStartWithSetpoint, OpcUaId_HasComponent);
UA_ASSERT(addStatus.isGood());
// Add Property "InputArguments"
sName = "StartWithSetpoint";
sNodeId = UaString("%1.%2").arg(m_pMethodStartWithSetpoint->nodeId().toString()).arg(sName);
pPropertyArg = new UaPropertyMethodArgument(
UaNodeId(sNodeId, nsIdx), // NodeId of the property
Ua_AccessLevel_CurrentRead, // Access level of the property
1, // Number of arguments
// Argument TemperatureSetPoint
pPropertyArg->setArgument(
0, // Index of the argument
"TemperatureSetPoint", // Name of the argument
UaNodeId(OpcUaId_Double),// Data type of the argument
OpcUa_ValueRanks_Scalar, // Array rank of the argument
nullarray, // Array dimensions of the argument
UaLocalizedText("en", "Controller set point for temperature")); // Description
// Add property to method
addStatus = pNodeManager->addNodeAndReference(m_pMethodStartWithSetpoint, pPropertyArg, OpcUaId_HasProperty);
UA_ASSERT(addStatus.isGood());
// New code ends
}

and to AirConditionerControllerObject::AirConditionerControllerObject

// AirConditionerControllerObject::AirConditionerControllerObject
...
UaStatus addStatus;
// New code begins
// Method helpers
UaPropertyMethodArgument* pPropertyArg = NULL;
UaUInt32Array nullarray;
OpcUa_Int16 nsIdx = pNodeManager->getNameSpaceIndex();
// New code ends
/**************************************************************
* Create the AirConditionerController components
*/
...
// Change value handling to get read and write calls to the node manager
// New code begins
// Add Method "StartWithSetpoint"
UaString sName = "StartWithSetpoint";
UaString sNodeId = UaString("%1.%2").arg(newNodeId.toString()).arg(sName);
m_pMethodStartWithSetpoint = new UaMethodGeneric(
sName,
UaNodeId(sNodeId, nsIdx),
m_defaultLocaleId);
addStatus = pNodeManager->addNodeAndReference(this, m_pMethodStartWithSetpoint, OpcUaId_HasComponent);
UA_ASSERT(addStatus.isGood());
// Add Property "InputArguments"
sName = "StartWithSetpoint";
sNodeId = UaString("%1.%2").arg(m_pMethodStartWithSetpoint->nodeId().toString()).arg(sName);
pPropertyArg = new UaPropertyMethodArgument(
UaNodeId(sNodeId, nsIdx), // NodeId of the property
Ua_AccessLevel_CurrentRead, // Access level of the property
2, // Number of arguments
// Argument TemperatureSetPoint
pPropertyArg->setArgument(
0, // Index of the argument
"TemperatureSetPoint", // Name of the argument
UaNodeId(OpcUaId_Double),// Data type of the argument
OpcUa_ValueRanks_Scalar, // Array rank of the argument
nullarray, // Array dimensions of the argument
UaLocalizedText("en", "Controller set point for temperature")); // Description
// Argument HumiditySetpoint
pPropertyArg->setArgument(
1, // Index of the argument
"HumiditySetpoint", // Name of the argument
UaNodeId(OpcUaId_Double),// Data type of the argument
OpcUa_ValueRanks_Scalar, // Array rank of the argument
nullarray, // Array dimensions of the argument
UaLocalizedText("en", "Controller set point for humidity")); // Description
// Add property to method
addStatus = pNodeManager->addNodeAndReference(m_pMethodStartWithSetpoint, pPropertyArg, OpcUaId_HasProperty);
UA_ASSERT(addStatus.isGood());
// New code ends
}

Complete call() Implementation in Sub Classes

Now we finish this lesson in completing call() as shown below for FurnaceControllerObject.

UaStatus FurnaceControllerObject::call(
UaMethod* pMethod,
const UaVariantArray& inputArguments,
UaVariantArray& /*outputArguments*/,
UaStatusCodeArray& inputArgumentResults,
UaDiagnosticInfos& /*inputArgumentDiag*/)
{
UaStatus ret;
if(pMethod)
{
// Check if we have the start method
if ( pMethod->nodeId() == m_pMethodStartWithSetpoint->nodeId())
{
// Number of input arguments must be 1
if ( inputArguments.length() != 1 )
{
ret = OpcUa_BadInvalidArgument;
}
else
{
inputArgumentResults.create(1);
// validate input parameter
if ( inputArguments[0].Datatype != OpcUaType_Double )
{
ret = OpcUa_BadInvalidArgument;
inputArgumentResults[0] = OpcUa_BadTypeMismatch;
}
else
{
// New code begins
UaVariant vTemp;
OpcUa_Double value;
ret = m_pCommIf->setControllerState(
m_deviceAddress,
BaCommunicationInterface::Ba_ControllerState_On );
if ( ret.isGood() )
{
// Setting TemperatureSetPoint
vTemp = inputArguments[0];
vTemp.toDouble(value);
ret = m_pCommIf->setControllerData(
m_deviceAddress,
1,
value);
}
// New code ends
}
}
}
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(
UaMethod* pMethod,
const UaVariantArray& inputArguments,
UaVariantArray& /*outputArguments*/,
UaStatusCodeArray& inputArgumentResults,
UaDiagnosticInfos& /*inputArgumentDiag*/)
{
UaStatus ret;
if(pMethod)
{
// Check if we have the start method
if ( pMethod->nodeId() == m_pMethodStartWithSetpoint->nodeId())
{
// Number of input arguments must be 2
if ( inputArguments.length() != 2 )
{
ret = OpcUa_BadInvalidArgument;
}
else
{
inputArgumentResults.create(2);
// validate input parameters
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;
}
if (ret.isGood())
{
// New code begins
UaVariant vTemp;
OpcUa_Double value;
ret = m_pCommIf->setControllerState(
m_deviceAddress,
BaCommunicationInterface::Ba_ControllerState_On );
if ( ret.isGood() )
{
// Setting TemperatureSetPoint
vTemp = inputArguments[0];
vTemp.toDouble(value);
ret = m_pCommIf->setControllerData(
m_deviceAddress,
1,
value);
// Setting HumiditySetPoint
vTemp = inputArguments[1];
vTemp.toDouble(value);
ret = m_pCommIf->setControllerData(
m_deviceAddress,
4,
value);
}
// New code ends
}
}
}
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

l4gettingstartedlesson04_call_startwithsetpoint_with_uaexpert.png