Historizing

Questions regarding the use of the ANSI C SDK for Server or Client development or integration into customer products ...

Moderator: uasdkc

Post Reply
macsurfer
Full Member
Full Member
Posts: 8
Joined: 09 Aug 2013, 12:26

Historizing

Post by macsurfer »

Hello,

I try to get into the Historic Part of the SDK and like to adapt Lesson07 to display more than one variable in the Historic View.
To achieve this I am going through the DemoServer and checked the variable ByteWithHistory and DoubleWithHistory.
Unfortunately there are also methods to Start/Stop Logging and the UaProvider_Demo_SimulateHistoryVariables, so it is quite complicated.
I understand that such an example could show all the possibilities of the SDK, but in my case I just like to start small.

So...,
I created a server with the UAModeler which contains a MeasurementDevice and 6 variables(Voltages, Currents), the server compiles and executes and I can drag&drop the variable to the DefaultDAView or the HistoryTrendView. So the variables have the right AccessLevel. Maybe one more thing, I like to identify the Nodes by Numeric_Id´s.

Now I like to adapt the method uaprovider_name_historyread and uaprovider_name_write.
Up to now I understand that the <uaprovider_name_write> is responsible to write the data to a log file using the datalogger and <uaprovider_name_historyread> is just to receive the data in the Client.

So my questions are:

What is the "concept" of having a UserDataCommon struct?

To explain at which problem I am right now I like to compare the Lesson07 and the Demo next.

In the Interface IFMETHODIMP(CustomProvider_WriteAsync)(UaServer_ProviderWriteContext* a_pWriteCtx) there´s a switch-case where one checks the UserDataTemperature, can I extend the program to check for several variables in here ?

Code: Select all

TemperatureSensor *pSensor = (TemperatureSensor*)pUserData;
          *pSensor->pValue = pValue->Value.Double;

Code: Select all

/* Iterate over nodes and check if the provider is responsible for processing them */
    for (i = 0; i < pReq->NoOfNodesToWrite; i++)
    ...
           if (pNode)
            {
               ...
                    OpcUa_Variant *pValue = &a_pWriteCtx->pRequest->NodesToWrite[i].Value.Value;

                    switch (pUserData->Type)
					                   {
                   case UserDataTemperature:
                        {
                            if (pValue->ArrayType == OpcUa_VariantArrayType_Scalar &&
                                pValue->Datatype == OpcUaType_Double)
                            {
                                TemperatureSensor *pSensor = (TemperatureSensor*)pUserData;
                                *pSensor->pValue = pValue->Value.Double;
                                pRes->Results[i] = OpcUa_Good;
                            }
                     ...
                    UaServer_WriteInternal(pNode, a_pWriteCtx, i);
        ...
    /* Send callback */
    UaServer_WriteComplete(a_pWriteCtx);


In the IFMETHODIMP(UaProvider_Demo_WriteAsync)(UaServer_ProviderWriteContext* a_pWriteCtx) there´s another option, I just read that this is used when methods are not "available" ?

Code: Select all

for (i = 0; i < pReq->NoOfNodesToWrite; i++)
    {
       ...
        if (pNode)
        {    
            /* handle special variables, which trigger actions.
            * this could better be done with methods, but if you don't have methods (only DA)
            * you can use this technique.
            */
            if (OpcUa_BaseNode_GetType(pNode) == eVariable
                && pReq->NodesToWrite[i].AttributeId == OpcUa_Attributes_Value)
            {
                OpcUa_NodeId nodeId;
                if (pNodeId->IdentifierType == OpcUa_IdentifierType_String)
                {
                    UaServer_CreateStringNodeId(&nodeId, Demo_Objects_Demo_SimulationActive);
                    if (OpcUa_NodeId_Compare(&nodeId, pNodeId) == 0)
                    {
                        /* validate datatype */
                        if (pValue->Datatype == OpcUaType_Boolean
                            && pValue->ArrayType == OpcUa_VariantArrayType_Scalar)
                        {
                          /* trigger action */
                            if (pValue->Value.Boolean)
                                UaProvider_Demo_StartSimulation();
                            else
                                UaProvider_Demo_StopSimulation();
                        }
                    ...

                    UaServer_CreateStringNodeId(&nodeId, Demo_Objects_Demo_History_DataLoggerActive);
                    if (OpcUa_NodeId_Compare(&nodeId, pNodeId) == 0)
                    {
                        /* validate datatype */
                        if (pValue->Datatype == OpcUaType_Boolean
                            && pValue->ArrayType == OpcUa_VariantArrayType_Scalar)
                        {
                            /* trigger action */
                            if (pValue->Value.Boolean)
                                UaProvider_Demo_StartLogging();
                            else
                                UaProvider_Demo_StopLogging();
                        }
            ...
            UaServer_WriteInternal(pNode, a_pWriteCtx, i);
        }
     ...
    /* send callback */
    UaServer_WriteComplete(a_pWriteCtx);
So my next question is about the UserDataType.

Code: Select all

#ifndef _UAPROVIDER_DEMO_SIMULATION_H_
#define _UAPROVIDER_DEMO_SIMULATION_H_ 1

#include <uaserver_config.h>
#include <uaserver_providers.h>

/** Enum of custom user data types */
enum _UserDataType
{
    UserDataTemperature,
    UserDataMachine,
    UserDataMachineSwitch,
    UserDataAnalogItem,
    UserDataHistoryDataLogger
};
typedef enum _UserDataType UserDataType;

/** All user data structs contain the same header with type information.
 * This concept is application specific and only an example.
* You can store whatever you like in UserData.
 */
struct _UserDataCommon
{
    UserDataType Type; /**< currently only the type info is needed in the common header */
};
typedef struct _UserDataCommon UserDataCommon;

struct _HistoryDataLogger
{
    /* user data header */
    UserDataType Type;
    /* user data HistoryDataLogger data */
    OpcUa_Int    DataLogger;
    OpcUa_Int    DataLogItem;
};
typedef struct _HistoryDataLogger HistoryDataLogger;

struct _TemperatureSensor
{
    /* user data header */
    UserDataType Type;
    /* user data temperature data */
    OpcUa_NodeId        NodeId;
    OpcUa_NodeId        HighAlarmId;
    OpcUa_NodeId        LowAlarmId;
    /* protocol information */
    OpcUa_Double       *pValue;
};
typedef struct _TemperatureSensor TemperatureSensor; 
would it be possible to add another struct like this and so get access to the data in the "NodeID loop"

Code: Select all

struct _Voltage_L1
{
    /* user data header */
    UserDataType Type;
    /* user data temperature data */
    OpcUa_NodeId        NodeId;
    /* protocol information */
    OpcUa_Float      *pValue;
};
typedef struct _Voltage_L1 Voltage_L1;
So when I use the Demo-like option I need to Setup and Start/Stop the logger, how can I use Numeric_Id´s here?

Code: Select all

void UaProvider_Demo_SetupDataLogger()
if (g_hDataLogger < 0) return;

    UaServer_CreateStringNodeId(&nodeId, Demo_Objects_Demo_History_ByteWithHistory);
    UaProvider_Demo_HistorizeItem(&nodeId);
    UaServer_CreateStringNodeId(&nodeId, Demo_Objects_Demo_History_DoubleWithHistory);
    UaProvider_Demo_HistorizeItem(&nodeId);
    ...
    UaServer_DataLogger_Start(g_hDataLogger);
    ...

Code: Select all

OpcUa_StatusCode UaProvider_Demo_StartLogging()
{
    OpcUa_NodeId             nodeId;
    ...
    *g_pDataLoggerActive = OpcUa_True;

    /* enable items to start sampling */
    UaServer_CreateStringNodeId(&nodeId, Demo_Objects_Demo_History_ByteWithHistory);
    UaProvider_Demo_EnableHistorizedItem(&nodeId, OpcUa_True);
    UaServer_CreateStringNodeId(&nodeId, Demo_Objects_Demo_History_DoubleWithHistory);
    UaProvider_Demo_EnableHistorizedItem(&nodeId, OpcUa_True);
    ...
}

User avatar
Support Team
Hero Member
Hero Member
Posts: 3068
Joined: 18 Mar 2011, 15:09

Re: Historizing

Post by Support Team »

Hello,

you have a lot of questions. I'll try to answer them all, but for the future it
would be easier for use if you create different threads for different
quesitions. So it is easier to answer and nothing can be forgotten.

Start/StopLogging is an implementation detail of our example. We don't want to
fill the disk with this example, so data logging is disabled by default. If you
want to play with History you can start logging by executing the according
method. You don't need this in your server. Your server could start logging directly when it's started.

Node attributes: Yes you have already mentioned it is important that the
UserAccessLevel is HistoryReadable, so that a client can read it. The
Historizing attribute indicates if the variable is currently historized (data
gets logged). Some people are confused about this two different things.
Historizing can be false (no logging) but there is still history that can be
read.

Provider functions: A provider has to implement UaProvider_name_WriteAsync.
This is the counterpart of UaProvider_name_ReadAsync. This is just DataAccess
and has nothing to do with history. This is just writing and reading the
current variable value.
Of course when you are logging values you want to log new values when they change.
Our SDK has the possibility to create an internal MonitoredItem for the
DataLogger. So everytime a value changes it gets logged. This uses the same
mechanism as for client subscriptions.
Another possibility would be to just log data periodically, e.g. every 2s.

UserData: in OPC UA all nodes in all services are identified using a unique
NodeId. When you use our SDK on toolkit level (this is the easy way) your are
using our predefined node "classes". The SDK can lookup node pointers by their
NodeId in an efficient way. Normally you need to add implementation specific
data to such nodes, e.g. protocol information of your subsystem. Therefore you
can create your own UserData struct with your information and attach this to a
node (OpcUa_BaseNode_Get/SetUserData). So it's easy to retrieve your data based
on a given NodeId.
In this case we also store data for the datalogger in such a UserData struct.
See this as an example on how to use UserData.

> In the Interface
> IFMETHODIMP(CustomProvider_WriteAsync)(UaServer_ProviderWriteContext*
> a_pWriteCtx) there´s a switch-case where one checks the UserDataTemperature,
> can I extend the program to check for several variables in here ?
Each variable can have it's own userdata. So there is no need to do that. But
yes, you can change and extend UserData as you need it. This "user" is you ;-)

> In the IFMETHODIMP(UaProvider_Demo_WriteAsync)(UaServer_ProviderWriteContext*
> a_pWriteCtx) there´s another option, I just read that this is used when
> methods are not "available" ?
We created this method StartLogging/StopLogging. That's the nice OO way that
OPC UA supports. But there may be simple clients that do not support calling methods
or old COM based OPC DA clients that are connected using the UaGateway which can only read and write variables. So we
added an additional option to trigger this method by writing a variable.

> So my next question about UserDataType: would it be possible to add another
> struct like this and so get access to the data in the "NodeID loop"
Yes. You can as many types as you want.
That's why our UserData example has this UserDataType field as the first member.
You can cast void* from OpcUa_BaseNode_GetUserData to UserDataCommon. Depending
on the type you can downcast to the correct struct. (ANSI C polymorphism)

> So when I use the Demo-like option I need to Setup and Start/Stop the logger,
> how can I use Numeric_Id´s here?

Code: Select all

...
UaServer_CreateNumericNodeId(&nodeId, 12345);
UaProvider_Demo_HistorizeItem(&nodeId);
...
See
http://doc.unifiedautomation.com/uasdkc ... dressSpace.
html#gaf947c48e84821f31080851273d1a0a32
Here you will find even more NodeId helper functions.

regards,
Unified Automation Support Team.

macsurfer
Full Member
Full Member
Posts: 8
Joined: 09 Aug 2013, 12:26

Re: Historizing

Post by macsurfer »

Hello,

thanks for your quick reply (especially on the weekend!) which helped me to get a bit further.
I am able to write the temperature AND the machine switch variable to the log file.
But I assume there´s a problem in my historyread- method because I can only "historyread" one variable not both.
If I "disable" the historyread for e.g. the temperature I can look/ update the machine switch values.
If I "disable" the historyread for the machine switch I can update the temperature values.

Here´s what I changed in the file "custom_provider_historyread.c".
I also changed the other files, but because I mainly copied the commands executed with the temperature data I skipped that part.

Code: Select all

...
/* process nodes */
    for (i = 0; i < a_pHistoryReadRawModifiedCtx->NoOfNodesToRead; i++)
...
/********************* Reading the Temperature history *********************************/
if (pUserData && pUserData->Type == UserDataTemperature)
{
TemperatureSensor *pTemperatureSensorData = (TemperatureSensor*)pUserData;
...
/**********************Reading the MachineSwitch history************************/
//if (pUserData && pUserData->Type == UserDataMachineSwitch)
//               {
//                   MachineSwitch *pMachineSwitch = (MachineSwitch*)pUserData;
//                   a_pHistoryReadRawModifiedCtx->pResponse->Results[i].StatusCode = UaServer_DataLogger_ReadValues(
//                   g_hDataLogger,
//                   pMachineSwitch->hDataLogItemMachineSwitch,
//                   a_pHistoryReadRawModifiedCtx->pHistoryReadRawModifiedDetails,
//                   a_pHistoryReadRawModifiedCtx->TimestampsToReturn,
//                   a_pHistoryReadRawModifiedCtx->ReleaseContinuationPoints,
//                   &a_pHistoryReadRawModifiedCtx->pNodesToRead[i],
//                   pHistoryResult);
 //               }
                    else
                    {
                        a_pHistoryReadRawModifiedCtx->pResponse->Results[i].StatusCode = OpcUa_BadNotReadable;
                    }
...
UaServer_HistoryReadRawModifiedComplete(a_pHistoryReadRawModifiedCtx);
So here an excerpt from the log file:

PrimaryKey DataType Value StatusCode SourceTimeStamp ServerTimeStamp
1 0x000b 25 0x00000000 2013-08-11T14:46:52.157Z 2013-08-11T14:46:52.157Z
1 0x000b 24.5 0x00000000 2013-08-11T14:46:52.358Z 2013-08-11T14:46:52.358Z
2 0x0001 0 0x00000000 2013-08-11T14:46:52.157Z 2013-08-11T14:46:52.157Z
1 0x000b 24 0x00000000 2013-08-11T14:46:52.658Z 2013-08-11T14:46:52.658Z
...
1 0x000b -4.5 0x00000000 2013-08-11T14:47:07.459Z 2013-08-11T14:47:07.459Z
2 0x0001 1 0x00000000 2013-08-11T14:47:07.159Z 2013-08-11T14:47:07.159Z
1 0x000b -4 0x00000000 2013-08-11T14:47:07.660Z 2013-08-11T14:47:07.660Z
1 0x000b -3.5 0x00000000 2013-08-11T14:47:07.960Z 2013-08-11T14:47:07.960Z
2 0x0001 0 0x00000000 2013-08-11T14:47:08.060Z 2013-08-11T14:47:08.060Z
1 0x000b -4 0x00000000 2013-08-11T14:47:08.160Z 2013-08-11T14:47:08.160Z

User avatar
Support Team
Hero Member
Hero Member
Posts: 3068
Joined: 18 Mar 2011, 15:09

Re: Historizing

Post by Support Team »

Hi,

I modified that lesson07 example so that it logs both, the HeaterSwitch and the Temperature.
I hope this helps.

macsurfer
Full Member
Full Member
Posts: 8
Joined: 09 Aug 2013, 12:26

Re: Historizing

Post by macsurfer »

Hello,

yes, the example is working now.

It helped me to understand the UserData- Concept and I should be able to transfer the technique to my own model.

When I was working with your code the compiler did following error which I could fix.
error C2039: 'GetNodeById': Ist kein Element von '_UaServer_pProviderCBInterface' ...\lesson07\custom_provider.c 884
I am using UaServer_GetNode instead and a the global variable.g_pCustomProvider.

Code: Select all

...
    UaServer_AddressSpace   *pAddressSpace  = &(g_pCustomProvider->AddressSpace);
    UaServer_GetNode(pAddressSpace, pId, &pNode);

    //pNode = UaServer_GetNodeById(pId);

    if (pNode)
    {
 ...

User avatar
Support Team
Hero Member
Hero Member
Posts: 3068
Joined: 18 Mar 2011, 15:09

Re: Historizing

Post by Support Team »

hi

UaServer_GetNodeById is a new convenience function introduced in V1.3.2.
See http://doc.unifiedautomation.com/uasdkc ... d64de7b6a8

I guess you are not using the latest version. But using UaServer_GetNode does the same, you just need more parameters.

regards,
Unified Automation Support Team

User avatar
Support Team
Hero Member
Hero Member
Posts: 3068
Joined: 18 Mar 2011, 15:09

Re: Historizing

Post by Support Team »

Hi,

if your questions are solved now, please mark this thread as solved by clicking on the green check mark.

regards,
Unified Automation Support Team

Post Reply