From 38f453955e5e6ccab3548951a86e764dc4eb2e5c Mon Sep 17 00:00:00 2001 From: Luigi <40570739+luibass92@users.noreply.github.com> Date: Wed, 17 Nov 2021 14:20:38 +0100 Subject: [PATCH] [REVIEW] Circular buffer implementation for historydata plugin (#4740) --- examples/CMakeLists.txt | 1 + .../tutorial_server_historicaldata_circular.c | 133 ++++++++ .../ua_history_data_backend_memory.c | 261 ++++++++++++++++ .../ua_history_data_gathering_default.c | 28 ++ .../historydata/history_data_backend_memory.h | 10 + .../history_data_gathering_default.h | 8 + tests/CMakeLists.txt | 4 + .../check_server_historical_data_circular.c | 294 ++++++++++++++++++ 8 files changed, 739 insertions(+) create mode 100644 examples/tutorial_server_historicaldata_circular.c create mode 100644 tests/server/check_server_historical_data_circular.c diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 4559d9a940d..e2ca238dcfc 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -165,6 +165,7 @@ add_example(server_loglevel server_loglevel.c) if(UA_ENABLE_HISTORIZING) add_example(tutorial_server_historicaldata tutorial_server_historicaldata.c) + add_example(tutorial_server_historicaldata_circular tutorial_server_historicaldata_circular.c) endif() if(UA_ENABLE_ENCRYPTION OR UA_ENABLE_ENCRYPTION STREQUAL "MBEDTLS" OR UA_ENABLE_ENCRYPTION STREQUAL "OPENSSL") diff --git a/examples/tutorial_server_historicaldata_circular.c b/examples/tutorial_server_historicaldata_circular.c new file mode 100644 index 00000000000..bc61f8327f0 --- /dev/null +++ b/examples/tutorial_server_historicaldata_circular.c @@ -0,0 +1,133 @@ +/* This work is licensed under a Creative Commons CCZero 1.0 Universal License. + * See http://creativecommons.org/publicdomain/zero/1.0/ for more information. + * + * Copyright 2019 (c) basysKom GmbH (Author: Peter Rustler) + * Copyright 2021 (c) luibass92 (Author: Luigi Bassetta) + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +static volatile UA_Boolean running = true; +static void stopHandler(int sign) { + (void)sign; + UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "received ctrl-c"); + running = false; +} + +int main(void) { + signal(SIGINT, stopHandler); + signal(SIGTERM, stopHandler); + + UA_Server *server = UA_Server_new(); + UA_ServerConfig *config = UA_Server_getConfig(server); + UA_ServerConfig_setDefault(config); + + /* We need a gathering for the plugin to constuct. + * The UA_HistoryDataGathering is responsible to collect data and store it to the database. + * We will use this gathering for one node, only. initialNodeIdStoreSize = 1 + * The store will NOT automatically grow if you register more than one node will return a UA_STATUS_BADOUTOFMEMORY. */ + UA_HistoryDataGathering gathering = UA_HistoryDataGathering_Circular(1); + + /* We set the responsible plugin in the configuration. UA_HistoryDatabase is + * the main plugin which handles the historical data service. */ + config->historyDatabase = UA_HistoryDatabase_default(gathering); + + /* Define the attribute of the uint32 variable node */ + UA_VariableAttributes attr = UA_VariableAttributes_default; + UA_UInt32 myUint32 = 40; + UA_Variant_setScalar(&attr.value, &myUint32, &UA_TYPES[UA_TYPES_UINT32]); + attr.description = UA_LOCALIZEDTEXT("en-US","myUintValue"); + attr.displayName = UA_LOCALIZEDTEXT("en-US","myUintValue"); + attr.dataType = UA_TYPES[UA_TYPES_UINT32].typeId; + /* We set the access level to also support history read + * This is what will be reported to clients */ + attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE | UA_ACCESSLEVELMASK_HISTORYREAD; + /* We also set this node to historizing, so the server internals also know from it. */ + attr.historizing = true; + + /* Add the variable node to the information model */ + UA_NodeId uint32NodeId = UA_NODEID_STRING(1, "myUintValue"); + UA_QualifiedName uint32Name = UA_QUALIFIEDNAME(1, "myUintValue"); + UA_NodeId parentNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER); + UA_NodeId parentReferenceNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES); + UA_NodeId outNodeId; + UA_NodeId_init(&outNodeId); + UA_StatusCode retval = UA_Server_addVariableNode(server, + uint32NodeId, + parentNodeId, + parentReferenceNodeId, + uint32Name, + UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), + attr, + NULL, + &outNodeId); + + UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, + "UA_Server_addVariableNode %s", UA_StatusCode_name(retval)); + + /* Now we define the settings for our node */ + UA_HistorizingNodeIdSettings setting; + + /* There is a memory based database plugin. We will use that. We just + * reserve space for 3 nodes with 10 values each. This will NOT automatically grow + * but will store data as a circular buffer of size 10. The 11th value will be + * stored replacing the oldest one and the process will continue like that. */ + setting.historizingBackend = UA_HistoryDataBackend_Memory_Circular(3, 10); + + /* We want the server to serve a maximum of 100 values per request. This + * value depend on the plattform you are running the server. A big server + * can serve more values, smaller ones less. */ + setting.maxHistoryDataResponseSize = 100; + + /* If we have a sensor which do not report updates + * and need to be polled we change the setting like that. + * The polling interval in ms. + * + setting.pollingInterval = 100; + * + * Set the update strategie to polling. + * + setting.historizingUpdateStrategy = UA_HISTORIZINGUPDATESTRATEGY_POLL; + */ + + /* If you want to insert the values to the database yourself, we can set the user strategy here. + * This is useful if you for example want a value stored, if a defined delta is reached. + * Then you should use a local monitored item with a fuzziness and store the value in the callback. + * + setting.historizingUpdateStrategy = UA_HISTORIZINGUPDATESTRATEGY_USER; + */ + + /* We want the values stored in the database, when the nodes value is + * set. */ + setting.historizingUpdateStrategy = UA_HISTORIZINGUPDATESTRATEGY_VALUESET; + + /* At the end we register the node for gathering data in the database. */ + retval = gathering.registerNodeId(server, gathering.context, &outNodeId, setting); + UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "registerNodeId %s", UA_StatusCode_name(retval)); + + /* If you use UA_HISTORIZINGUPDATESTRATEGY_POLL, then start the polling. + * + retval = gathering.startPoll(server, gathering.context, &outNodeId); + UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "startPoll %s", UA_StatusCode_name(retval)); + */ + retval = UA_Server_run(server, &running); + UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "UA_Server_run %s", UA_StatusCode_name(retval)); + /* + * If you use UA_HISTORIZINGUPDATESTRATEGY_POLL, then stop the polling. + * + retval = gathering.stopPoll(server, gathering.context, &outNodeId); + UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "stopPoll %s", UA_StatusCode_name(retval)); + */ + + UA_Server_delete(server); + return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/plugins/historydata/ua_history_data_backend_memory.c b/plugins/historydata/ua_history_data_backend_memory.c index 74421638699..3e99794ec33 100644 --- a/plugins/historydata/ua_history_data_backend_memory.c +++ b/plugins/historydata/ua_history_data_backend_memory.c @@ -3,6 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. * * Copyright 2018 (c) basysKom GmbH (Author: Peter Rustler) + * Copyright 2021 (c) luibass92 (Author: Luigi Bassetta) */ #include @@ -26,6 +27,8 @@ typedef struct { UA_DataValueMemoryStoreItem **dataStore; size_t storeEnd; size_t storeSize; + /* New field useful for circular buffer management */ + size_t lastInserted; } UA_NodeIdStoreContextItem_backend_memory; static void @@ -603,3 +606,261 @@ UA_HistoryDataBackend_Memory_clear(UA_HistoryDataBackend *backend) UA_MemoryStoreContext_delete(ctx); memset(backend, 0, sizeof(UA_HistoryDataBackend)); } + +/* Circular buffer implementation */ + +static UA_NodeIdStoreContextItem_backend_memory * +getNewNodeIdContext_backend_memory_Circular(UA_MemoryStoreContext *context, + UA_Server *server, + const UA_NodeId *nodeId) { + UA_MemoryStoreContext *ctx = (UA_MemoryStoreContext *)context; + if(ctx->storeEnd >= ctx->storeSize) { + return NULL; + } + UA_NodeIdStoreContextItem_backend_memory *item = &ctx->dataStore[ctx->storeEnd]; + UA_NodeId_copy(nodeId, &item->nodeId); + UA_DataValueMemoryStoreItem **store = (UA_DataValueMemoryStoreItem **)UA_calloc(ctx->initialStoreSize, sizeof(UA_DataValueMemoryStoreItem *)); + if(!store) { + UA_NodeIdStoreContextItem_clear(item); + return NULL; + } + item->dataStore = store; + item->storeSize = ctx->initialStoreSize; + item->storeEnd = 0; + ++ctx->storeEnd; + return item; +} + +static UA_NodeIdStoreContextItem_backend_memory * +getNodeIdStoreContextItem_backend_memory_Circular(UA_MemoryStoreContext *context, + UA_Server *server, + const UA_NodeId *nodeId) { + for(size_t i = 0; i < context->storeEnd; ++i) { + if(UA_NodeId_equal(nodeId, &context->dataStore[i].nodeId)) { + return &context->dataStore[i]; + } + } + return getNewNodeIdContext_backend_memory_Circular(context, server, nodeId); +} + +static UA_StatusCode +serverSetHistoryData_backend_memory_Circular(UA_Server *server, + void *context, + const UA_NodeId *sessionId, + void *sessionContext, + const UA_NodeId *nodeId, + UA_Boolean historizing, + const UA_DataValue *value) { + UA_NodeIdStoreContextItem_backend_memory *item = getNodeIdStoreContextItem_backend_memory_Circular((UA_MemoryStoreContext *)context, server, nodeId); + if(item == NULL) { + return UA_STATUSCODE_BADOUTOFMEMORY; + } + if(item->lastInserted >= item->storeSize) { + /* If the buffer size is overcomed, push new elements from the start of the buffer */ + item->lastInserted = 0; + } + UA_DateTime timestamp = 0; + if(value->hasSourceTimestamp) { + timestamp = value->sourceTimestamp; + } else if(value->hasServerTimestamp) { + timestamp = value->serverTimestamp; + } else { + timestamp = UA_DateTime_now(); + } + UA_DataValueMemoryStoreItem *newItem = (UA_DataValueMemoryStoreItem *)UA_calloc(1, sizeof(UA_DataValueMemoryStoreItem)); + newItem->timestamp = timestamp; + UA_DataValue_copy(value, &newItem->value); + + /* This implementation does NOT sort values by timestamp */ + + if(item->dataStore[item->lastInserted] != NULL) { + UA_DataValueMemoryStoreItem_clear(item->dataStore[item->lastInserted]); + UA_free(item->dataStore[item->lastInserted]); + } + item->dataStore[item->lastInserted] = newItem; + ++item->lastInserted; + + if(item->storeEnd < item->storeSize) { + ++item->storeEnd; + } + + return UA_STATUSCODE_GOOD; +} + +static size_t +getResultSize_service_Circular(const UA_HistoryDataBackend *backend, UA_Server *server, + const UA_NodeId *sessionId, void *sessionContext, + const UA_NodeId *nodeId, UA_DateTime start, + UA_DateTime end, UA_UInt32 numValuesPerNode, + UA_Boolean returnBounds, size_t *startIndex, + size_t *endIndex, UA_Boolean *addFirst, + UA_Boolean *addLast, UA_Boolean *reverse) { + *startIndex = 0; + *endIndex = backend->lastIndex(server, backend->context, sessionId, sessionContext, nodeId); + *addFirst = false; + *addLast = false; + if(end == LLONG_MIN) { + *reverse = false; + } else if(start == LLONG_MIN) { + *reverse = true; + } else { + *reverse = end < start; + } + + size_t size = 0; + const UA_NodeIdStoreContextItem_backend_memory *item = getNodeIdStoreContextItem_backend_memory_Circular((UA_MemoryStoreContext *)backend->context, server, nodeId); + if(item == NULL) { + size = 0; + } else { + size = item->storeEnd; + } + return size; +} + +static UA_StatusCode +getHistoryData_service_Circular(UA_Server *server, + const UA_NodeId *sessionId, + void *sessionContext, + const UA_HistoryDataBackend *backend, + const UA_DateTime start, + const UA_DateTime end, + const UA_NodeId *nodeId, + size_t maxSize, + UA_UInt32 numValuesPerNode, + UA_Boolean returnBounds, + UA_TimestampsToReturn timestampsToReturn, + UA_NumericRange range, + UA_Boolean releaseContinuationPoints, + const UA_ByteString *continuationPoint, + UA_ByteString *outContinuationPoint, + UA_HistoryData *historyData) { + size_t *resultSize = &historyData->dataValuesSize; + UA_DataValue **result = &historyData->dataValues; + size_t skip = 0; + UA_ByteString backendContinuationPoint; + UA_ByteString_init(&backendContinuationPoint); + if(continuationPoint->length > 0) { + if(continuationPoint->length < sizeof(size_t)) + return UA_STATUSCODE_BADCONTINUATIONPOINTINVALID; + skip = *((size_t *)(continuationPoint->data)); + backendContinuationPoint.length = continuationPoint->length - sizeof(size_t); + backendContinuationPoint.data = continuationPoint->data + sizeof(size_t); + } + size_t storeEnd = backend->getEnd(server, backend->context, sessionId, sessionContext, nodeId); + size_t startIndex; + size_t endIndex; + UA_Boolean addFirst; + UA_Boolean addLast; + UA_Boolean reverse; + size_t _resultSize = getResultSize_service_Circular(backend, + server, + sessionId, + sessionContext, + nodeId, + start, + end, + numValuesPerNode == 0 ? 0 : numValuesPerNode + (UA_UInt32)skip, + returnBounds, + &startIndex, + &endIndex, + &addFirst, + &addLast, + &reverse); + *resultSize = _resultSize - skip; + if(*resultSize > maxSize) { + *resultSize = maxSize; + } + UA_DataValue *outResult = (UA_DataValue *)UA_Array_new(*resultSize, &UA_TYPES[UA_TYPES_DATAVALUE]); + if(!outResult) { + *resultSize = 0; + return UA_STATUSCODE_BADOUTOFMEMORY; + } + *result = outResult; + size_t counter = 0; + if(addFirst) { + if(skip == 0) { + outResult[counter].hasStatus = true; + outResult[counter].status = UA_STATUSCODE_BADBOUNDNOTFOUND; + outResult[counter].hasSourceTimestamp = true; + if(start == LLONG_MIN) { + outResult[counter].sourceTimestamp = end; + } else { + outResult[counter].sourceTimestamp = start; + } + ++counter; + } + } + UA_ByteString backendOutContinuationPoint; + UA_ByteString_init(&backendOutContinuationPoint); + if(endIndex != storeEnd && startIndex != storeEnd) { + size_t retval = 0; + size_t valueSize = *resultSize - counter; + if(valueSize + skip > _resultSize - addFirst - addLast) { + if(skip == 0) { + valueSize = _resultSize - addFirst - addLast; + } else { + valueSize = _resultSize - skip - addLast; + } + } + UA_StatusCode ret = UA_STATUSCODE_GOOD; + if(valueSize > 0) + ret = backend->copyDataValues(server, + backend->context, + sessionId, + sessionContext, + nodeId, + startIndex, + endIndex, + reverse, + valueSize, + range, + releaseContinuationPoints, + &backendContinuationPoint, + &backendOutContinuationPoint, + &retval, + &outResult[counter]); + if(ret != UA_STATUSCODE_GOOD) { + UA_Array_delete(outResult, *resultSize, &UA_TYPES[UA_TYPES_DATAVALUE]); + *result = NULL; + *resultSize = 0; + return ret; + } + counter += retval; + } + if(addLast && counter < *resultSize) { + outResult[counter].hasStatus = true; + outResult[counter].status = UA_STATUSCODE_BADBOUNDNOTFOUND; + outResult[counter].hasSourceTimestamp = true; + if(start == LLONG_MIN && storeEnd != backend->firstIndex(server, backend->context, sessionId, sessionContext, nodeId)) { + outResult[counter].sourceTimestamp = backend->getDataValue(server, backend->context, sessionId, sessionContext, nodeId, endIndex)->sourceTimestamp - UA_DATETIME_SEC; + } else if(end == LLONG_MIN && storeEnd != backend->firstIndex(server, backend->context, sessionId, sessionContext, nodeId)) { + outResult[counter].sourceTimestamp = backend->getDataValue(server, backend->context, sessionId, sessionContext, nodeId, endIndex)->sourceTimestamp + UA_DATETIME_SEC; + } else { + outResult[counter].sourceTimestamp = end; + } + } + // there are more values + if(skip + *resultSize < _resultSize + // there are not more values for this request, but there are more values in + // database + || (backendOutContinuationPoint.length > 0 && numValuesPerNode != 0) + // we deliver just one value which is a FIRST/LAST value + || (skip == 0 && addFirst == true && *resultSize == 1)) { + if(UA_ByteString_allocBuffer(outContinuationPoint, backendOutContinuationPoint.length + sizeof(size_t)) != UA_STATUSCODE_GOOD) { + return UA_STATUSCODE_BADOUTOFMEMORY; + } + *((size_t *)(outContinuationPoint->data)) = skip + *resultSize; + if(backendOutContinuationPoint.length > 0) + memcpy(outContinuationPoint->data + sizeof(size_t), backendOutContinuationPoint.data, backendOutContinuationPoint.length); + } + UA_ByteString_clear(&backendOutContinuationPoint); + return UA_STATUSCODE_GOOD; +} + +UA_HistoryDataBackend +UA_HistoryDataBackend_Memory_Circular(size_t initialNodeIdStoreSize, size_t initialDataStoreSize) { + UA_HistoryDataBackend result = UA_HistoryDataBackend_Memory(initialNodeIdStoreSize, initialDataStoreSize); + result.serverSetHistoryData = &serverSetHistoryData_backend_memory_Circular; + result.getHistoryData = &getHistoryData_service_Circular; + return result; +} diff --git a/plugins/historydata/ua_history_data_gathering_default.c b/plugins/historydata/ua_history_data_gathering_default.c index ada881fd1ce..db55473c4a0 100644 --- a/plugins/historydata/ua_history_data_gathering_default.c +++ b/plugins/historydata/ua_history_data_gathering_default.c @@ -3,6 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. * * Copyright 2018 (c) basysKom GmbH (Author: Peter Rustler) + * Copyright 2021 (c) luibass92 (Author: Luigi Bassetta) */ #include @@ -228,3 +229,30 @@ UA_HistoryDataGathering_Default(size_t initialNodeIdStoreSize) gathering.context = context; return gathering; } + +/* Circular buffer implementation */ + +static UA_StatusCode +registerNodeId_gathering_circular(UA_Server *server, void *context, + const UA_NodeId *nodeId, + const UA_HistorizingNodeIdSettings setting) { + UA_NodeIdStoreContext *ctx = (UA_NodeIdStoreContext *)context; + if(getNodeIdStoreContextItem_gathering_default(ctx, nodeId)) { + return UA_STATUSCODE_BADNODEIDEXISTS; + } + if(ctx->storeEnd >= ctx->storeSize || !ctx->dataStore) { + return UA_STATUSCODE_BADOUTOFMEMORY; + } + UA_NodeId_copy(nodeId, &ctx->dataStore[ctx->storeEnd].nodeId); + size_t current = ctx->storeEnd; + ctx->dataStore[current].setting = setting; + ++ctx->storeEnd; + return UA_STATUSCODE_GOOD; +} + +UA_HistoryDataGathering +UA_HistoryDataGathering_Circular(size_t initialNodeIdStoreSize) { + UA_HistoryDataGathering gathering = UA_HistoryDataGathering_Default(initialNodeIdStoreSize); + gathering.registerNodeId = ®isterNodeId_gathering_circular; + return gathering; +} diff --git a/plugins/include/open62541/plugin/historydata/history_data_backend_memory.h b/plugins/include/open62541/plugin/historydata/history_data_backend_memory.h index 7c0ec59eb57..17f1b87c9b7 100644 --- a/plugins/include/open62541/plugin/historydata/history_data_backend_memory.h +++ b/plugins/include/open62541/plugin/historydata/history_data_backend_memory.h @@ -3,6 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. * * Copyright 2018 (c) basysKom GmbH (Author: Peter Rustler) + * Copyright 2021 (c) luibass92 (Author: Luigi Bassetta) */ #ifndef UA_HISTORYDATABACKEND_MEMORY_H_ @@ -17,6 +18,15 @@ _UA_BEGIN_DECLS UA_HistoryDataBackend UA_EXPORT UA_HistoryDataBackend_Memory(size_t initialNodeIdStoreSize, size_t initialDataStoreSize); +/* This function construct a UA_HistoryDataBackend which implements a circular buffer in memory. + * + * initialNodeIdStoreSize is the maximum number of NodeIds that will be historized. This number cannot be overcomed. + * initialDataStoreSize is the maximum number of UA_DataValueMemoryStoreItem that will be saved in the circular buffer for a particular NodeId. + * Subsequent UA_DataValueMemoryStoreItem will be saved replacing the oldest ones following the logic of circular buffers. + */ +UA_HistoryDataBackend UA_EXPORT +UA_HistoryDataBackend_Memory_Circular(size_t initialNodeIdStoreSize, size_t initialDataStoreSize); + void UA_EXPORT UA_HistoryDataBackend_Memory_clear(UA_HistoryDataBackend *backend); diff --git a/plugins/include/open62541/plugin/historydata/history_data_gathering_default.h b/plugins/include/open62541/plugin/historydata/history_data_gathering_default.h index cfd039e4bf9..7985bc763f2 100644 --- a/plugins/include/open62541/plugin/historydata/history_data_gathering_default.h +++ b/plugins/include/open62541/plugin/historydata/history_data_gathering_default.h @@ -3,6 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. * * Copyright 2018 (c) basysKom GmbH (Author: Peter Rustler) + * Copyright 2021 (c) luibass92 (Author: Luigi Bassetta) */ #ifndef UA_HISTORYDATAGATHERING_DEFAULT_H_ @@ -15,6 +16,13 @@ _UA_BEGIN_DECLS UA_HistoryDataGathering UA_EXPORT UA_HistoryDataGathering_Default(size_t initialNodeIdStoreSize); +/* This function construct a UA_HistoryDataGathering which implements a circular buffer in memory. + * + * initialNodeIdStoreSize is the maximum number of NodeIds for which the data will be gathered. This number cannot be overcomed. + */ +UA_HistoryDataGathering UA_EXPORT +UA_HistoryDataGathering_Circular(size_t initialNodeIdStoreSize); + _UA_END_DECLS #endif /* UA_HISTORYDATAGATHERING_DEFAULT_H_ */ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 59dcc745bf3..ac9e32dd447 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -346,6 +346,10 @@ if(UA_ENABLE_HISTORIZING) add_executable(check_server_historical_data server/check_server_historical_data.c $ $) target_link_libraries(check_server_historical_data ${LIBS}) add_test_valgrind(server_historical_data ${TESTS_BINARY_DIR}/check_server_historical_data) + + add_executable(check_server_historical_data_circular server/check_server_historical_data_circular.c $ $) + target_link_libraries(check_server_historical_data_circular ${LIBS}) + add_test_valgrind(server_historical_data_circular ${TESTS_BINARY_DIR}/check_server_historical_data_circular) endif() add_executable(check_session server/check_session.c $ $) diff --git a/tests/server/check_server_historical_data_circular.c b/tests/server/check_server_historical_data_circular.c new file mode 100644 index 00000000000..5a40e232d97 --- /dev/null +++ b/tests/server/check_server_historical_data_circular.c @@ -0,0 +1,294 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright 2021 (c) luibass92 (Author: Luigi Bassetta) + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "client/ua_client_internal.h" +#include "server/ua_server_internal.h" + +#include + +#include "testing_clock.h" +#include "testing_networklayers.h" +#include "thread_wrapper.h" +#include + +static UA_Server *server; +#ifdef UA_ENABLE_HISTORIZING +static UA_HistoryDataGathering *gathering; +#endif +static UA_Boolean running; +static THREAD_HANDLE server_thread; +static MUTEX_HANDLE serverMutex; + +static UA_Client *client; +static UA_NodeId parentNodeId; +static UA_NodeId parentReferenceNodeId; +static UA_NodeId outNodeId; + +static void serverMutexLock(void) { + if (!(MUTEX_LOCK(serverMutex))) { + fprintf(stderr, "Mutex cannot be locked.\n"); + exit(1); + } +} + +static void serverMutexUnlock(void) { + if (!(MUTEX_UNLOCK(serverMutex))) { + fprintf(stderr, "Mutex cannot be unlocked.\n"); + exit(1); + } +} + +THREAD_CALLBACK(serverloop) { + while(running) { + serverMutexLock(); + UA_Server_run_iterate(server, false); + serverMutexUnlock(); + } + return 0; +} + +static void setup(void) { + if (!(MUTEX_INIT(serverMutex))) { + fprintf(stderr, "Server mutex was not created correctly.\n"); + exit(1); + } + running = true; + + server = UA_Server_new(); + UA_ServerConfig *config = UA_Server_getConfig(server); + UA_ServerConfig_setDefault(config); + +#ifdef UA_ENABLE_HISTORIZING + gathering = (UA_HistoryDataGathering*)UA_calloc(1, sizeof(UA_HistoryDataGathering)); + *gathering = UA_HistoryDataGathering_Circular(1); + config->historyDatabase = UA_HistoryDatabase_default(*gathering); +#endif + + UA_StatusCode retval = UA_Server_run_startup(server); + if(retval != UA_STATUSCODE_GOOD) { + fprintf(stderr, "Error while calling Server_run_startup. %s\n", UA_StatusCode_name(retval)); + UA_Server_delete(server); + exit(1); + } + + THREAD_CREATE(server_thread, serverloop); + /* Define the attribute of the uint32 variable node */ + UA_VariableAttributes attr = UA_VariableAttributes_default; + UA_UInt32 myUint32 = 40; + UA_Variant_setScalar(&attr.value, &myUint32, &UA_TYPES[UA_TYPES_UINT32]); + attr.description = UA_LOCALIZEDTEXT("en-US","the answer"); + attr.displayName = UA_LOCALIZEDTEXT("en-US","the answer"); + attr.dataType = UA_TYPES[UA_TYPES_UINT32].typeId; + attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE | UA_ACCESSLEVELMASK_HISTORYREAD | UA_ACCESSLEVELMASK_HISTORYWRITE; + attr.historizing = true; + + /* Add the variable node to the information model */ + UA_NodeId uint32NodeId = UA_NODEID_STRING(1, "the.answer"); + UA_QualifiedName uint32Name = UA_QUALIFIEDNAME(1, "the answer"); + parentNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER); + parentReferenceNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES); + UA_NodeId_init(&outNodeId); + retval = UA_Server_addVariableNode(server, uint32NodeId, parentNodeId, + parentReferenceNodeId, uint32Name, + UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), + attr, NULL, &outNodeId); + if (retval != UA_STATUSCODE_GOOD) { + fprintf(stderr, "Error adding variable node. %s\n", UA_StatusCode_name(retval)); + UA_Server_delete(server); + exit(1); + } + + client = UA_Client_new(); + UA_ClientConfig_setDefault(UA_Client_getConfig(client)); + retval = UA_Client_connect(client, "opc.tcp://localhost:4840"); + if (retval != UA_STATUSCODE_GOOD) { + fprintf(stderr, "Client can not connect to opc.tcp://localhost:4840. %s\n", UA_StatusCode_name(retval)); + UA_Client_delete(client); + UA_Server_delete(server); + exit(1); + } + + UA_Client_recv = client->connection.recv; + client->connection.recv = UA_Client_recvTesting; +} + +static void teardown(void) { + /* cleanup */ + UA_Client_disconnect(client); + UA_Client_delete(client); + running = false; + THREAD_JOIN(server_thread); + UA_NodeId_clear(&parentNodeId); + UA_NodeId_clear(&parentReferenceNodeId); + UA_NodeId_clear(&outNodeId); + UA_Server_run_shutdown(server); + UA_Server_delete(server); +#ifdef UA_ENABLE_HISTORIZING + UA_free(gathering); +#endif + if (!MUTEX_DESTROY(serverMutex)) { + fprintf(stderr, "Server mutex was not destroyed correctly.\n"); + exit(1); + } +} + +#ifdef UA_ENABLE_HISTORIZING + +#include +#include "ua_session.h" + +static UA_StatusCode +setUInt32(UA_Client *thisClient, UA_NodeId node, UA_UInt32 value) +{ + UA_Variant variant; + UA_Variant_setScalar(&variant, &value, &UA_TYPES[UA_TYPES_UINT32]); + return UA_Client_writeValueAttribute(thisClient, node, &variant); +} + + +void +Service_HistoryRead(UA_Server *server, UA_Session *session, + const UA_HistoryReadRequest *request, + UA_HistoryReadResponse *response); + +static void +requestHistory(UA_DateTime start, + UA_DateTime end, + UA_HistoryReadResponse * response, + UA_UInt32 numValuesPerNode, + UA_Boolean returnBounds, + UA_ByteString *continuationPoint) +{ + UA_ReadRawModifiedDetails *details = UA_ReadRawModifiedDetails_new(); + details->startTime = start; + details->endTime = end; + details->isReadModified = false; + details->numValuesPerNode = numValuesPerNode; + details->returnBounds = returnBounds; + + UA_HistoryReadValueId *valueId = UA_HistoryReadValueId_new(); + UA_NodeId_copy(&outNodeId, &valueId->nodeId); + if (continuationPoint) + UA_ByteString_copy(continuationPoint, &valueId->continuationPoint); + + UA_HistoryReadRequest request; + UA_HistoryReadRequest_init(&request); + request.historyReadDetails.encoding = UA_EXTENSIONOBJECT_DECODED; + request.historyReadDetails.content.decoded.type = &UA_TYPES[UA_TYPES_READRAWMODIFIEDDETAILS]; + request.historyReadDetails.content.decoded.data = details; + + request.timestampsToReturn = UA_TIMESTAMPSTORETURN_BOTH; + + request.nodesToReadSize = 1; + request.nodesToRead = valueId; + + UA_LOCK(&server->serviceMutex); + Service_HistoryRead(server, &server->adminSession, &request, response); + UA_UNLOCK(&server->serviceMutex); + UA_HistoryReadRequest_clear(&request); +} + + +START_TEST(Server_HistorizingStrategyValueSet) +{ + // init to a defined value + UA_StatusCode retval = setUInt32(client, outNodeId, 43); + ck_assert_str_eq(UA_StatusCode_name(retval), UA_StatusCode_name(UA_STATUSCODE_GOOD)); + + // set a data backend + UA_HistorizingNodeIdSettings setting; + setting.historizingBackend = UA_HistoryDataBackend_Memory_Circular(3, 10); + setting.maxHistoryDataResponseSize = 10; + setting.historizingUpdateStrategy = UA_HISTORIZINGUPDATESTRATEGY_VALUESET; + serverMutexLock(); + retval = gathering->registerNodeId(server, gathering->context, &outNodeId, setting); + serverMutexUnlock(); + ck_assert_str_eq(UA_StatusCode_name(retval), UA_StatusCode_name(UA_STATUSCODE_GOOD)); + + // Fill the data overcoming the buffer size and starting to write new values replacing the old ones. + // The circular buffer size is 10, the number of elements historized is 15 (from 0 to 14). So the final buffer will be: + // + // | 10 | 11 | 12 | 13 | 14 | 5 | 6 | 7 | 8 | 9 | + // + UA_fakeSleep(100); + UA_DateTime start = UA_DateTime_now(); + UA_fakeSleep(100); + for (UA_UInt32 i = 0; i < 15; ++i) { + retval = setUInt32(client, outNodeId, i); + ck_assert_str_eq(UA_StatusCode_name(retval), UA_StatusCode_name(UA_STATUSCODE_GOOD)); + UA_fakeSleep(100); + } + UA_DateTime end = UA_DateTime_now(); + + // request + UA_HistoryReadResponse response; + UA_HistoryReadResponse_init(&response); + requestHistory(start, end, &response, 0, false, NULL); + + // test the response + ck_assert_str_eq(UA_StatusCode_name(response.responseHeader.serviceResult), UA_StatusCode_name(UA_STATUSCODE_GOOD)); + ck_assert_uint_eq(response.resultsSize, 1); + for (size_t i = 0; i < response.resultsSize; ++i) { + ck_assert_str_eq(UA_StatusCode_name(response.results[i].statusCode), UA_StatusCode_name(UA_STATUSCODE_GOOD)); + ck_assert_uint_eq(response.results[i].historyData.encoding, UA_EXTENSIONOBJECT_DECODED); + ck_assert(response.results[i].historyData.content.decoded.type == &UA_TYPES[UA_TYPES_HISTORYDATA]); + UA_HistoryData * data = (UA_HistoryData *)response.results[i].historyData.content.decoded.data; + ck_assert(data->dataValuesSize > 0); + for (size_t j = 0; j < data->dataValuesSize; ++j) { + ck_assert(data->dataValues[j].sourceTimestamp >= start && data->dataValues[j].sourceTimestamp < end); + ck_assert_uint_eq(data->dataValues[j].hasSourceTimestamp, true); + ck_assert_str_eq(UA_StatusCode_name(data->dataValues[j].status), UA_StatusCode_name(UA_STATUSCODE_GOOD)); + ck_assert_uint_eq(data->dataValues[j].hasValue, true); + ck_assert(data->dataValues[j].value.type == &UA_TYPES[UA_TYPES_UINT32]); + UA_UInt32 * value = (UA_UInt32 *)data->dataValues[j].value.data; + if(j >= 5) + ck_assert_uint_eq(*value, j); + else + ck_assert_uint_eq(*value, j + 10); + } + } + UA_HistoryReadResponse_clear(&response); + UA_HistoryDataBackend_Memory_clear(&setting.historizingBackend); +} +END_TEST + +#endif /*UA_ENABLE_HISTORIZING*/ + +static Suite* testSuite_Client(void) +{ + Suite *s = suite_create("Server Historical Data"); + TCase *tc_server = tcase_create("Server Historical Data Circular"); + tcase_add_checked_fixture(tc_server, setup, teardown); +#ifdef UA_ENABLE_HISTORIZING + tcase_add_test(tc_server, Server_HistorizingStrategyValueSet); +#endif /* UA_ENABLE_HISTORIZING */ + suite_add_tcase(s, tc_server); + + return s; +} + +int main(void) +{ + Suite *s = testSuite_Client(); + SRunner *sr = srunner_create(s); + srunner_set_fork_status(sr, CK_NOFORK); + srunner_run_all(sr,CK_NORMAL); + int number_failed = srunner_ntests_failed(sr); + srunner_free(sr); + return (number_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE; +}