ANSI C UA Server SDK  1.5.0.312
 All Data Structures Functions Variables Typedefs Enumerations Enumerator Modules Pages
Lesson 4: Adding Support for Methods

This lesson will show how to provide Methods in the Address Space.

Files used in this lesson:

Step 1: Add Method to MachineType

Figure 4-1 shows the machine type and the method that we will create in this lesson.

Figure 4-1 Machine Type

gettingstarted1_lesson04_machine_objecttypeall.png

First of all we create the InstanceDeclaration node for the MachineType by adding the method SetSwitchState as component to the object type. This is done in the method CustomProvider_CreateMachineType (see custom_provider.c). Additionally, we mark the method as mandatory for instances of the MachineType.

/* Create switch method */
a_pStartingNodeId->Identifier.Numeric++;
typeNodeId.Identifier.Numeric = OpcUaId_MethodNode;
referenceNodeId.Identifier.Numeric = OpcUaId_HasComponent;
uStatus = UaServer_CreateNode(pAddressSpace,
(OpcUa_BaseNode**)&pMethod,
pNewMachineType,
a_pStartingNodeId,
OpcUa_NodeClass_Method,
&referenceNodeId,
OpcUa_Null,
"SetSwitchState",
"SetSwitchState",
"Switches the machine on or off and returns the current temperature");
OpcUa_GotoErrorIfBad(uStatus);
/* Set ModellingRule Mandatory for switch method child object */
nodeId.NamespaceIndex = 0;
nodeId.Identifier.Numeric = OpcUaId_ModellingRule_Mandatory;
referenceNodeId.Identifier.Numeric = OpcUaId_HasModellingRule;
uStatus = OpcUa_BaseNode_AddReferenceToNodeId(pMethod, &nodeId, &referenceNodeId);
OpcUa_GotoErrorIfBad(uStatus);

The new method shall have one input and one output argument:

  • The input argument will specify the switch position to be set by the method,
  • the output argument will return the current value of the machine's temperature sensor.
/* Input arguments */
a_pStartingNodeId->Identifier.Numeric++;
uStatus = UaServer_CreateProperty(pAddressSpace,
&pProperty,
(OpcUa_BaseNode*)pMethod,
a_pStartingNodeId->Identifier.Numeric,
g_uCustomProvider_NamespaceIndex,
"InputArguments");
OpcUa_GotoErrorIfBad(uStatus);
OpcUa_BaseNode_GetBrowseName(pProperty)->NamespaceIndex = 0;
OpcUa_Variable_SetDataType_Numeric(pProperty, OpcUaId_Argument, 0);
OpcUa_Variable_SetValueRank(pProperty, OpcUa_ValueRanks_OneDimension);
pArrayDimensions = (OpcUa_UInt32*)OpcUa_Alloc(sizeof(OpcUa_UInt32));
pArrayDimensions[0] = 1;
OpcUa_Variable_AttachArrayDimensions(pProperty, pArrayDimensions, 1);
pValue = OpcUa_Variable_GetValue(pProperty);
pValue->Datatype = OpcUaType_ExtensionObject;
pValue->ArrayType = OpcUa_VariantArrayType_Array;
pValue->Value.Array.Length = 1;
pValue->Value.Array.Value.ExtensionObjectArray = (OpcUa_ExtensionObject*)OpcUa_Alloc(sizeof(OpcUa_ExtensionObject));
OpcUa_EncodeableObject_CreateExtension(&OpcUa_Argument_EncodeableType,
&pValue->Value.Array.Value.ExtensionObjectArray[0],
(OpcUa_Void**)&pArgument);
pArgument->ValueRank = OpcUa_ValueRanks_Scalar;
pArgument->DataType.Identifier.Numeric = OpcUaId_Boolean;
OpcUa_String_StrnCpy(&pArgument->Name,
OpcUa_String_FromCString("SwitchState"),
OPCUA_STRING_LENDONTCARE);
OpcUa_String_StrnCpy(&pArgument->Description.Locale,
OpcUa_String_FromCString("en"),
OPCUA_STRING_LENDONTCARE);
OpcUa_String_StrnCpy(&pArgument->Description.Text,
OpcUa_String_FromCString("The state to set the switch to"),
OPCUA_STRING_LENDONTCARE);
/* Output arguments */
a_pStartingNodeId->Identifier.Numeric++;
uStatus = UaServer_CreateProperty(pAddressSpace,
&pProperty,
(OpcUa_BaseNode*)pMethod,
a_pStartingNodeId->Identifier.Numeric,
g_uCustomProvider_NamespaceIndex,
"OutputArguments");
OpcUa_GotoErrorIfBad(uStatus);
OpcUa_BaseNode_GetBrowseName(pProperty)->NamespaceIndex = 0;
OpcUa_Variable_SetDataType_Numeric(pProperty, OpcUaId_Argument, 0);
OpcUa_Variable_SetValueRank(pProperty, OpcUa_ValueRanks_OneDimension);
pArrayDimensions = (OpcUa_UInt32*)OpcUa_Alloc(sizeof(OpcUa_UInt32));
pArrayDimensions[0] = 1;
OpcUa_Variable_AttachArrayDimensions(pProperty, pArrayDimensions, 1);
pValue = OpcUa_Variable_GetValue(pProperty);
pValue->Datatype = OpcUaType_ExtensionObject;
pValue->ArrayType = OpcUa_VariantArrayType_Array;
pValue->Value.Array.Length = 1;
pValue->Value.Array.Value.ExtensionObjectArray = (OpcUa_ExtensionObject*)OpcUa_Alloc(sizeof(OpcUa_ExtensionObject));
OpcUa_EncodeableObject_CreateExtension(&OpcUa_Argument_EncodeableType,
&pValue->Value.Array.Value.ExtensionObjectArray[0],
(OpcUa_Void**)&pArgument);
pArgument->ValueRank = OpcUa_ValueRanks_Scalar;
pArgument->DataType.Identifier.Numeric = OpcUaId_Double;
OpcUa_String_StrnCpy(&pArgument->Name,
OpcUa_String_FromCString("CurrentTemperature"),
OPCUA_STRING_LENDONTCARE);
OpcUa_String_StrnCpy(&pArgument->Description.Locale,
OpcUa_String_FromCString("en"),
OPCUA_STRING_LENDONTCARE);
OpcUa_String_StrnCpy(&pArgument->Description.Text,
OpcUa_String_FromCString("The current temperature of the machine"),
OPCUA_STRING_LENDONTCARE);

Step 2: Add Method to the Machine Instance

Now we need to add the method as child of the machine instance we created. This is done in CustomProvider_CreateMachine, similarly to the creation of the method of the MachineType in Step 1: Add Method to MachineType. For identifying the method when being called, we set the user data of the machine also as user data of the newly created method.

/* Set new machine struct as user data of the method */
OpcUa_BaseNode_SetUserData(pMethod, pNewMachine);

Step 3: Implementing the Handling of Method Calls

Whenever a method is called, the CallAsync service handler of the provider is invoked, so we need to implement this handler in our provider in order to process method calls. The function CustomProvider_CallAsync is set as the handler routine in CustomProvider_Initialize.

IFMETHODIMP(CustomProvider_Initialize)(UaServer_Provider *a_pProvider,
UaServer_pProviderInterface *a_pProviderInterface)
{
...
a_pProviderInterface->CallAsync = CustomProvider_CallAsync;

The implementation of the CustomProvider_CallAsync function checks if the method call should be processed by the provider. Therefore it checks wheter MethodId and ObjectId are nodes belonging to the provider (see custom_provider_call.c).

IFMETHODIMP(CustomProvider_CallAsync)(UaServer_ProviderCallContext *a_pCallContext)
{
...
for (i = 0; i < pRequest->NoOfMethodsToCall; i++)
{
if (pRequest->MethodsToCall[i].MethodId.NamespaceIndex == g_uCustomProvider_NamespaceIndex &&
pRequest->MethodsToCall[i].ObjectId.NamespaceIndex == g_uCustomProvider_NamespaceIndex)
{
UaServer_GetNode(pAddressSpace, &pRequest->MethodsToCall[i].MethodId, &pNode);
if (pNode != OpcUa_Null)
{

If this is the case, the UserData of the method being called is retrieved and evaluated. In case the UserData is of type machine, the method call is processed. Additionally we check whether the method was called on the corresponding machine. For this purpose we compare the NodeId of the machine with the ObjectId passed in the method call.

UserDataCommon *pUserData = (UserDataCommon*)OpcUa_BaseNode_GetUserData(pNode);
if (pUserData != OpcUa_Null)
{
switch (pUserData->Type)
{
case UserDataMachine:
{
Machine *pMachine = (Machine*)pUserData;
/* Check if the method was called on the appropriate Machine */
if (OpcUa_NodeId_Compare(&pRequest->MethodsToCall[i].ObjectId, &pMachine->nodeId) != 0)
{
pResponse->Results[i].StatusCode = OpcUa_BadNotFound;
break;
}

If the method call passes a valid input arguments, the HeaterSwitch position is set to the position passed in the method call. After this the output argument is created and set to the current temperature of the temperature sensor.

/* Check input arguments */
if (pRequest->MethodsToCall[i].NoOfInputArguments == 1)
{
OpcUa_Variant *pInArg = &pRequest->MethodsToCall[i].InputArguments[0];
OpcUa_CallMethodResult *pResult = &pResponse->Results[i];
pResult->NoOfInputArgumentResults = 1;
pResult->InputArgumentResults = (OpcUa_StatusCode*)OpcUa_Alloc(sizeof(OpcUa_StatusCode));
if (pInArg->ArrayType == OpcUa_VariantArrayType_Scalar &&
pInArg->Datatype == OpcUaType_Boolean)
{
/* Set switch state */
*pMachine->pHeaterSwitch->pValue = pInArg->Value.Boolean;
pResult->InputArgumentResults[0] = OpcUa_Good;
}
else
{
pResult->InputArgumentResults[0] = OpcUa_BadTypeMismatch;
}
/* Fill output arguments */
pResult->NoOfOutputArguments = 1;
pResult->OutputArguments = (OpcUa_Variant*)OpcUa_Alloc(sizeof(OpcUa_Variant));
OpcUa_Variant_Initialize(&pResult->OutputArguments[0]);
pResult->OutputArguments[0].Datatype = OpcUaType_Double;
pResult->OutputArguments[0].Value.Double = *pMachine->pTemperatureSensor->pValue;
pResult->StatusCode = OpcUa_Good;
}
else
{
pResponse->Results[i].StatusCode = OpcUa_BadArgumentsMissing;
}

Step 4: Calling the Method with a Client

After implementing the method handling, we can test it by using a client to call the method.

Figure 4-2 Monitoring a Variable using UA Expert

gettingstarted1_lesson04_monitor_heaterswitch_with_uaexpert.png

Drag and drop HeaterSwitch into the Default DA View tab. Note that the value is “false”. Right click the SetSwitchState method in the Address Space browser and click Call....

Figure 4-3 Calling SetSwitchState

gettingstarted1_lesson04_call_switchmethod_with_uaexpert.png

alt="Call SetSwitchState">

Set the SwitchState argument to “true” and click Call. The value of HeaterSwitch in the DA View changes to “true” and the current temperature is shown in the output argument field.