Bladeren bron

Implementation of a history data plugin

Peter Rustler 6 jaren geleden
bovenliggende
commit
e74af216d2

+ 8 - 0
CMakeLists.txt

@@ -258,10 +258,18 @@ endif()
 if(UA_ENABLE_HISTORIZING)
     set(historizing_exported_headers
         ${PROJECT_SOURCE_DIR}/include/ua_plugin_historydatabase.h
+        ${PROJECT_SOURCE_DIR}/plugins/historydata/ua_plugin_history_data_backend.h
+        ${PROJECT_SOURCE_DIR}/plugins/historydata/ua_plugin_history_data_gathering.h
         )
     set(historizing_default_plugin_headers
+        ${PROJECT_SOURCE_DIR}/plugins/historydata/ua_historydatabackend_memory.h
+        ${PROJECT_SOURCE_DIR}/plugins/historydata/ua_historydatagathering_default.h
+        ${PROJECT_SOURCE_DIR}/plugins/historydata/ua_historydatabase_default.h
         )
     set(historizing_default_plugin_sources
+        ${PROJECT_SOURCE_DIR}/plugins/historydata/ua_historydatabackend_memory.c
+        ${PROJECT_SOURCE_DIR}/plugins/historydata/ua_historydatagathering_default.c
+        ${PROJECT_SOURCE_DIR}/plugins/historydata/ua_historydatabase_default.c
         )
 endif()
 

+ 435 - 0
plugins/historydata/ua_historydatabackend_memory.c

@@ -0,0 +1,435 @@
+/* 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 2018 (c) basysKom GmbH <opensource@basyskom.com> (Author: Peter Rustler)
+ */
+
+#include "ua_historydatabackend_memory.h"
+#include <limits.h>
+#include <string.h>
+
+typedef struct {
+    UA_DateTime timestamp;
+    UA_DataValue value;
+} UA_DataValueMemoryStoreItem;
+
+static void
+UA_DataValueMemoryStoreItem_deleteMembers(UA_DataValueMemoryStoreItem* item) {
+    UA_DateTime_deleteMembers(&item->timestamp);
+    UA_DataValue_deleteMembers(&item->value);
+}
+
+typedef struct {
+    UA_NodeId nodeId;
+    UA_DataValueMemoryStoreItem **dataStore;
+    size_t storeEnd;
+    size_t storeSize;
+} UA_NodeIdStoreContextItem_backend_memory;
+
+static void
+UA_NodeIdStoreContextItem_deleteMembers(UA_NodeIdStoreContextItem_backend_memory* item) {
+    UA_NodeId_deleteMembers(&item->nodeId);
+    for (size_t i = 0; i < item->storeEnd; ++i) {
+        UA_DataValueMemoryStoreItem_deleteMembers(item->dataStore[i]);
+        UA_free(item->dataStore[i]);
+    }
+    UA_free(item->dataStore);
+}
+
+typedef struct {
+    UA_NodeIdStoreContextItem_backend_memory *dataStore;
+    size_t storeEnd;
+    size_t storeSize;
+    size_t initialStoreSize;
+} UA_MemoryStoreContext;
+
+static void
+UA_MemoryStoreContext_deleteMembers(UA_MemoryStoreContext* ctx) {
+    for (size_t i = 0; i < ctx->storeEnd; ++i) {
+        UA_NodeIdStoreContextItem_deleteMembers(&ctx->dataStore[i]);
+    }
+    UA_free(ctx->dataStore);
+    memset(ctx, 0, sizeof(UA_MemoryStoreContext));
+}
+
+static UA_NodeIdStoreContextItem_backend_memory *
+getNewNodeIdContext_backend_memory(UA_MemoryStoreContext* context,
+                                   UA_Server *server,
+                                   const UA_NodeId *nodeId) {
+    UA_MemoryStoreContext *ctx = (UA_MemoryStoreContext*)context;
+    if (ctx->storeEnd >= ctx->storeSize) {
+        size_t newStoreSize = ctx->storeSize * 2;
+        if (newStoreSize == 0)
+            return NULL;
+        ctx->dataStore = (UA_NodeIdStoreContextItem_backend_memory*)UA_realloc(ctx->dataStore,  (newStoreSize * sizeof(UA_NodeIdStoreContextItem_backend_memory)));
+        if (!ctx->dataStore) {
+            ctx->storeSize = 0;
+            return false;
+        }
+        ctx->storeSize = newStoreSize;
+    }
+    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_deleteMembers(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(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(context, server, nodeId);
+}
+
+static UA_Boolean
+binarySearch_backend_memory(const UA_NodeIdStoreContextItem_backend_memory* item,
+                            const UA_DateTime timestamp,
+                            size_t *index) {
+    if (item->storeEnd == 0) {
+        *index = item->storeEnd;
+        return false;
+    }
+    size_t min = 0;
+    size_t max = item->storeEnd - 1;
+    while (min <= max) {
+        *index = (min + max) / 2;
+        if (item->dataStore[*index]->timestamp == timestamp) {
+            return true;
+        } else if (item->dataStore[*index]->timestamp < timestamp) {
+            if (*index == item->storeEnd - 1) {
+                *index = item->storeEnd;
+                return false;
+            }
+            min = *index + 1;
+        } else {
+            if (*index == 0)
+                return false;
+            max = *index - 1;
+        }
+    }
+    *index = min;
+    return false;
+
+}
+
+static size_t
+resultSize_backend_memory(UA_Server *server,
+                          void *context,
+                          const UA_NodeId *sessionId,
+                          void *sessionContext,
+                          const UA_NodeId * nodeId,
+                          size_t startIndex,
+                          size_t endIndex) {
+    const UA_NodeIdStoreContextItem_backend_memory* item = getNodeIdStoreContextItem_backend_memory((UA_MemoryStoreContext*)context, server, nodeId);
+    if (item->storeEnd == 0
+            || startIndex == item->storeEnd
+            || endIndex == item->storeEnd)
+        return 0;
+    return endIndex - startIndex + 1;
+}
+
+static size_t
+getDateTimeMatch_backend_memory(UA_Server *server,
+                                void *context,
+                                const UA_NodeId *sessionId,
+                                void *sessionContext,
+                                const UA_NodeId * nodeId,
+                                const UA_DateTime timestamp,
+                                const MatchStrategy strategy) {
+    const UA_NodeIdStoreContextItem_backend_memory* item = getNodeIdStoreContextItem_backend_memory((UA_MemoryStoreContext*)context, server, nodeId);
+    size_t current;
+    UA_Boolean retval = binarySearch_backend_memory(item, timestamp, &current);
+
+    if ((strategy == MATCH_EQUAL
+         || strategy == MATCH_EQUAL_OR_AFTER
+         || strategy == MATCH_EQUAL_OR_BEFORE)
+            && retval)
+        return current;
+    switch (strategy) {
+    case MATCH_AFTER:
+        if (retval)
+            return current+1;
+        return current;
+    case MATCH_EQUAL_OR_AFTER:
+        return current;
+    case MATCH_EQUAL_OR_BEFORE:
+        // retval == true aka "equal" is handled before
+        // Fall through if !retval
+    case MATCH_BEFORE:
+        if (current > 0)
+            return current-1;
+        else
+            return item->storeEnd;
+    default:
+        break;
+    }
+    return item->storeEnd;
+}
+
+
+static UA_StatusCode
+serverSetHistoryData_backend_memory(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((UA_MemoryStoreContext*)context, server, nodeId);
+
+    if (item->storeEnd >= item->storeSize) {
+        size_t newStoreSize = item->storeSize == 0 ? INITIAL_MEMORY_STORE_SIZE : item->storeSize * 2;
+        item->dataStore = (UA_DataValueMemoryStoreItem **)UA_realloc(item->dataStore,  (newStoreSize * sizeof(UA_DataValueMemoryStoreItem*)));
+        if (!item->dataStore) {
+            item->storeSize = 0;
+            return UA_STATUSCODE_BADOUTOFMEMORY;
+        }
+        item->storeSize = newStoreSize;
+    }
+    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);
+    size_t index = getDateTimeMatch_backend_memory(server,
+                                                   context,
+                                                   NULL,
+                                                   NULL,
+                                                   nodeId,
+                                                   timestamp,
+                                                   MATCH_EQUAL_OR_AFTER);
+    if (item->storeEnd > 0 && index < item->storeEnd) {
+        memmove(&item->dataStore[index+1], &item->dataStore[index], sizeof(UA_DataValueMemoryStoreItem*) * (item->storeEnd - index));
+    }
+    item->dataStore[index] = newItem;
+    ++item->storeEnd;
+    return UA_STATUSCODE_GOOD;
+}
+
+static void
+UA_MemoryStoreContext_delete(UA_MemoryStoreContext* ctx) {
+    UA_MemoryStoreContext_deleteMembers(ctx);
+    UA_free(ctx);
+}
+
+static size_t
+getEnd_backend_memory(UA_Server *server,
+                      void *context,
+                      const UA_NodeId *sessionId,
+                      void *sessionContext,
+                      const UA_NodeId * nodeId) {
+    const UA_NodeIdStoreContextItem_backend_memory* item = getNodeIdStoreContextItem_backend_memory((UA_MemoryStoreContext*)context, server, nodeId);;
+    return item->storeEnd;
+}
+
+static size_t
+lastIndex_backend_memory(UA_Server *server,
+                         void *context,
+                         const UA_NodeId *sessionId,
+                         void *sessionContext,
+                         const UA_NodeId * nodeId) {
+    const UA_NodeIdStoreContextItem_backend_memory* item = getNodeIdStoreContextItem_backend_memory((UA_MemoryStoreContext*)context, server, nodeId);;
+    return item->storeEnd - 1;
+}
+
+static size_t
+firstIndex_backend_memory(UA_Server *server,
+                          void *context,
+                          const UA_NodeId *sessionId,
+                          void *sessionContext,
+                          const UA_NodeId * nodeId) {
+    return 0;
+}
+
+static UA_Boolean
+boundSupported_backend_memory(UA_Server *server,
+                              void *context,
+                              const UA_NodeId *sessionId,
+                              void *sessionContext,
+                              const UA_NodeId * nodeId) {
+    return true;
+}
+
+static UA_Boolean
+timestampsToReturnSupported_backend_memory(UA_Server *server,
+                                           void *context,
+                                           const UA_NodeId *sessionId,
+                                           void *sessionContext,
+                                           const UA_NodeId *nodeId,
+                                           const UA_TimestampsToReturn timestampsToReturn) {
+    const UA_NodeIdStoreContextItem_backend_memory* item = getNodeIdStoreContextItem_backend_memory((UA_MemoryStoreContext*)context, server, nodeId);;
+    if (item->storeEnd == 0) {
+        return true;
+    }
+    if (timestampsToReturn == UA_TIMESTAMPSTORETURN_NEITHER
+            || timestampsToReturn == UA_TIMESTAMPSTORETURN_INVALID
+            || (timestampsToReturn == UA_TIMESTAMPSTORETURN_SERVER
+                && !item->dataStore[0]->value.hasServerTimestamp)
+            || (timestampsToReturn == UA_TIMESTAMPSTORETURN_SOURCE
+                && !item->dataStore[0]->value.hasSourceTimestamp)
+            || (timestampsToReturn == UA_TIMESTAMPSTORETURN_BOTH
+                && !(item->dataStore[0]->value.hasSourceTimestamp
+                     && item->dataStore[0]->value.hasServerTimestamp))) {
+        return false;
+    }
+    return true;
+}
+
+static const UA_DataValue*
+getDataValue_backend_memory(UA_Server *server,
+                            void *context,
+                            const UA_NodeId *sessionId,
+                            void *sessionContext,
+                            const UA_NodeId * nodeId, size_t index) {
+    const UA_NodeIdStoreContextItem_backend_memory* item = getNodeIdStoreContextItem_backend_memory((UA_MemoryStoreContext*)context, server, nodeId);;
+    return &item->dataStore[index]->value;
+}
+
+static UA_StatusCode
+UA_DataValue_backend_copyRange(const UA_DataValue *src, UA_DataValue *dst,
+                               const UA_NumericRange range)
+{
+    memcpy(dst, src, sizeof(UA_DataValue));
+    if (src->hasValue)
+        return UA_Variant_copyRange(&src->value, &dst->value, range);
+    return UA_STATUSCODE_BADDATAUNAVAILABLE;
+}
+
+static UA_StatusCode
+copyDataValues_backend_memory(UA_Server *server,
+                              void *context,
+                              const UA_NodeId *sessionId,
+                              void *sessionContext,
+                              const UA_NodeId * nodeId,
+                              size_t startIndex,
+                              size_t endIndex,
+                              UA_Boolean reverse,
+                              size_t maxValues,
+                              UA_NumericRange range,
+                              UA_Boolean releaseContinuationPoints,
+                              const UA_ByteString *continuationPoint,
+                              UA_ByteString *outContinuationPoint,
+                              size_t * providedValues,
+                              UA_DataValue * values)
+{
+    size_t skip = 0;
+    if (continuationPoint->length > 0) {
+        if (continuationPoint->length == sizeof(size_t)) {
+            skip = *((size_t*)(continuationPoint->data));
+        } else {
+            return UA_STATUSCODE_BADCONTINUATIONPOINTINVALID;
+        }
+    }
+    const UA_NodeIdStoreContextItem_backend_memory* item = getNodeIdStoreContextItem_backend_memory((UA_MemoryStoreContext*)context, server, nodeId);;
+    size_t index = startIndex;
+    size_t counter = 0;
+    size_t skipedValues = 0;
+    if (reverse) {
+        while (index >= endIndex && index < item->storeEnd && counter < maxValues) {
+            if (skipedValues++ >= skip) {
+                if (range.dimensionsSize > 0) {
+                    UA_DataValue_backend_copyRange(&item->dataStore[index]->value, &values[counter], range);
+                } else {
+                    UA_DataValue_copy(&item->dataStore[index]->value, &values[counter]);
+                }
+                ++counter;
+            }
+            --index;
+        }
+    } else {
+        while (index <= endIndex && counter < maxValues) {
+            if (skipedValues++ >= skip) {
+                if (range.dimensionsSize > 0) {
+                    UA_DataValue_backend_copyRange(&item->dataStore[index]->value, &values[counter], range);
+                } else {
+                    UA_DataValue_copy(&item->dataStore[index]->value, &values[counter]);
+                }
+                ++counter;
+            }
+            ++index;
+        }
+    }
+
+    if (providedValues)
+        *providedValues = counter;
+
+    if ((!reverse && (endIndex-startIndex-skip+1) > counter) || (reverse && (startIndex-endIndex-skip+1) > counter)) {
+        outContinuationPoint->length = sizeof(size_t);
+        size_t t = sizeof(size_t);
+        outContinuationPoint->data = (UA_Byte*)UA_malloc(t);
+        *((size_t*)(outContinuationPoint->data)) = skip + counter;
+    }
+
+    return UA_STATUSCODE_GOOD;
+}
+
+static void
+deleteMembers_backend_memory(UA_HistoryDataBackend *backend)
+{
+    if (backend == NULL || backend->context == NULL)
+        return;
+    UA_MemoryStoreContext_deleteMembers((UA_MemoryStoreContext*)backend->context);
+}
+
+
+
+UA_HistoryDataBackend
+UA_HistoryDataBackend_Memory(size_t initialNodeIdStoreSize, size_t initialDataStoreSize) {
+    if (initialNodeIdStoreSize == 0)
+        initialNodeIdStoreSize = 1;
+    if (initialDataStoreSize == 0)
+        initialDataStoreSize = 1;
+    UA_HistoryDataBackend result;
+    memset(&result, 0, sizeof(UA_HistoryDataBackend));
+    UA_MemoryStoreContext *ctx = (UA_MemoryStoreContext *)UA_calloc(1, sizeof(UA_MemoryStoreContext));
+    if (!ctx)
+        return result;
+    ctx->dataStore = (UA_NodeIdStoreContextItem_backend_memory*)UA_calloc(initialNodeIdStoreSize, sizeof(UA_NodeIdStoreContextItem_backend_memory));
+    ctx->initialStoreSize = initialDataStoreSize;
+    ctx->storeSize = initialNodeIdStoreSize;
+    ctx->storeEnd = 0;
+    result.serverSetHistoryData = &serverSetHistoryData_backend_memory;
+    result.resultSize = &resultSize_backend_memory;
+    result.getEnd = &getEnd_backend_memory;
+    result.lastIndex = &lastIndex_backend_memory;
+    result.firstIndex = &firstIndex_backend_memory;
+    result.getDateTimeMatch = &getDateTimeMatch_backend_memory;
+    result.copyDataValues = &copyDataValues_backend_memory;
+    result.getDataValue = &getDataValue_backend_memory;
+    result.boundSupported = &boundSupported_backend_memory;
+    result.timestampsToReturnSupported = &timestampsToReturnSupported_backend_memory;
+    result.deleteMembers = deleteMembers_backend_memory;
+    result.getHistoryData = NULL;
+    result.context = ctx;
+    return result;
+}
+
+void
+UA_HistoryDataBackend_Memory_deleteMembers(UA_HistoryDataBackend *backend)
+{
+    UA_MemoryStoreContext *ctx = (UA_MemoryStoreContext*)backend->context;
+    UA_MemoryStoreContext_delete(ctx);
+    memset(backend, 0, sizeof(UA_HistoryDataBackend));
+}

+ 30 - 0
plugins/historydata/ua_historydatabackend_memory.h

@@ -0,0 +1,30 @@
+/* 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 2018 (c) basysKom GmbH <opensource@basyskom.com> (Author: Peter Rustler)
+ */
+
+#ifndef UA_HISTORYDATABACKEND_MEMORY_H_
+#define UA_HISTORYDATABACKEND_MEMORY_H_
+
+#include "ua_plugin_history_data_backend.h"
+
+#define INITIAL_MEMORY_STORE_SIZE 1000
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+
+UA_HistoryDataBackend UA_EXPORT
+UA_HistoryDataBackend_Memory(size_t initialNodeIdStoreSize, size_t initialDataStoreSize);
+
+void UA_EXPORT
+UA_HistoryDataBackend_Memory_deleteMembers(UA_HistoryDataBackend *backend);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* UA_HISTORYDATABACKEND_MEMORY_H_ */

+ 459 - 0
plugins/historydata/ua_historydatabase_default.c

@@ -0,0 +1,459 @@
+/* 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 2018 (c) basysKom GmbH <opensource@basyskom.com> (Author: Peter Rustler)
+ */
+
+#include "ua_historydatabase_default.h"
+#include <limits.h>
+
+typedef struct {
+    UA_HistoryDataGathering gathering;
+} UA_HistoryDatabaseContext_default;
+
+static size_t
+getResultSize_service_default(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)
+{
+    size_t storeEnd = backend->getEnd(server, backend->context, sessionId, sessionContext, nodeId);
+    *startIndex = storeEnd;
+    *endIndex = storeEnd;
+    *addFirst = false;
+    *addLast = false;
+    if (end == LLONG_MIN) {
+        *reverse = false;
+    } else if (start == LLONG_MIN) {
+        *reverse = true;
+    } else {
+        *reverse = end < start;
+    }
+    UA_Boolean equal = start == end;
+    size_t size = 0;
+    if (storeEnd > 0) {
+        if (equal) {
+            if (returnBounds) {
+                *startIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, start, MATCH_EQUAL_OR_BEFORE);
+                if (*startIndex == storeEnd) {
+                    *startIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, start, MATCH_AFTER);
+                    *addFirst = true;
+                }
+                *endIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, start, MATCH_AFTER);
+                size = backend->resultSize(server, backend->context, sessionId, sessionContext, nodeId, *startIndex, *endIndex);
+            } else {
+                *startIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, start, MATCH_EQUAL);
+                *endIndex = *startIndex;
+                if (*startIndex == storeEnd)
+                    size = 0;
+                else
+                    size = 1;
+            }
+        } else if (start == LLONG_MIN) {
+            *endIndex = 0;
+            if (returnBounds) {
+                *addLast = true;
+                *startIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, end, MATCH_EQUAL_OR_AFTER);
+                if (*startIndex == storeEnd) {
+                    *startIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, end, MATCH_EQUAL_OR_BEFORE);
+                    *addFirst = true;
+                }
+            } else {
+                *startIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, end, MATCH_EQUAL_OR_BEFORE);
+            }
+            size = backend->resultSize(server, backend->context, sessionId, sessionContext, nodeId, *endIndex, *startIndex);
+        } else if (end == LLONG_MIN) {
+            *endIndex = storeEnd - 1;
+            if (returnBounds) {
+                *addLast = true;
+                *startIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, start, MATCH_EQUAL_OR_BEFORE);
+                if (*startIndex == storeEnd) {
+                    *startIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, start, MATCH_AFTER);
+                    *addFirst = true;
+                }
+            } else {
+                *startIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, start, MATCH_EQUAL_OR_AFTER);
+            }
+            size = backend->resultSize(server, backend->context, sessionId, sessionContext, nodeId, *startIndex, *endIndex);
+        } else if (*reverse) {
+            if (returnBounds) {
+                *startIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, start, MATCH_EQUAL_OR_AFTER);
+                if (*startIndex == storeEnd) {
+                    *addFirst = true;
+                    *startIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, start, MATCH_BEFORE);
+                }
+                *endIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, end, MATCH_EQUAL_OR_BEFORE);
+                if (*endIndex == storeEnd) {
+                    *addLast = true;
+                    *endIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, end, MATCH_AFTER);
+                }
+            } else {
+                *startIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, start, MATCH_EQUAL_OR_BEFORE);
+                *endIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, end, MATCH_AFTER);
+            }
+            size = backend->resultSize(server, backend->context, sessionId, sessionContext, nodeId, *endIndex, *startIndex);
+        } else {
+            if (returnBounds) {
+                *startIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, start, MATCH_EQUAL_OR_BEFORE);
+                if (*startIndex == storeEnd) {
+                    *addFirst = true;
+                    *startIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, start, MATCH_AFTER);
+                }
+                *endIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, end, MATCH_EQUAL_OR_AFTER);
+                if (*endIndex == storeEnd) {
+                    *addLast = true;
+                    *endIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, end, MATCH_BEFORE);
+                }
+            } else {
+                *startIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, start, MATCH_EQUAL_OR_AFTER);
+                *endIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, end, MATCH_BEFORE);
+            }
+            size = backend->resultSize(server, backend->context, sessionId, sessionContext, nodeId, *startIndex, *endIndex);
+        }
+    } else if (returnBounds) {
+        *addLast = true;
+        *addFirst = true;
+    }
+
+    if (*addLast)
+        ++size;
+    if (*addFirst)
+        ++size;
+
+    if (numValuesPerNode > 0 && size > numValuesPerNode) {
+        size = numValuesPerNode;
+        *addLast = false;
+    }
+    return size;
+}
+
+static UA_StatusCode
+getHistoryData_service_default(const UA_HistoryDataBackend* backend,
+                               const UA_DateTime start,
+                               const UA_DateTime end,
+                               UA_Server *server,
+                               const UA_NodeId *sessionId,
+                               void *sessionContext,
+                               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,
+                               size_t *resultSize,
+                               UA_DataValue ** result)
+{
+    size_t skip = 0;
+    UA_ByteString backendContinuationPoint;
+    UA_ByteString_init(&backendContinuationPoint);
+    if (continuationPoint->length > 0) {
+        if (continuationPoint->length >= sizeof(size_t)) {
+            skip = *((size_t*)(continuationPoint->data));
+            if (continuationPoint->length > 0) {
+                backendContinuationPoint.length = continuationPoint->length - sizeof(size_t);
+                backendContinuationPoint.data = continuationPoint->data + sizeof(size_t);
+            }
+        } else {
+            return UA_STATUSCODE_BADCONTINUATIONPOINTINVALID;
+        }
+    }
+
+    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_default(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;
+        memcpy(outContinuationPoint->data + sizeof(size_t), backendOutContinuationPoint.data, backendOutContinuationPoint.length);
+    }
+    UA_ByteString_deleteMembers(&backendOutContinuationPoint);
+    return UA_STATUSCODE_GOOD;
+}
+
+static void
+readRaw_service_default(UA_Server *server,
+                        void *context,
+                        const UA_NodeId *sessionId,
+                        void *sessionContext,
+                        const UA_RequestHeader *requestHeader,
+                        const UA_ReadRawModifiedDetails *historyReadDetails,
+                        UA_TimestampsToReturn timestampsToReturn,
+                        UA_Boolean releaseContinuationPoints,
+                        size_t nodesToReadSize,
+                        const UA_HistoryReadValueId *nodesToRead,
+                        UA_HistoryReadResponse *response,
+                        UA_HistoryData * const * const historyData)
+{
+    UA_HistoryDatabaseContext_default *ctx = (UA_HistoryDatabaseContext_default*)context;
+    for (size_t i = 0; i < nodesToReadSize; ++i) {
+        UA_Byte accessLevel = 0;
+        UA_Server_readAccessLevel(server,
+                                  nodesToRead[i].nodeId,
+                                  &accessLevel);
+        if (!(accessLevel & UA_ACCESSLEVELMASK_HISTORYREAD)) {
+            response->results[i].statusCode = UA_STATUSCODE_BADUSERACCESSDENIED;
+            continue;
+        }
+
+        UA_Boolean historizing = false;
+        UA_Server_readHistorizing(server,
+                                  nodesToRead[i].nodeId,
+                                  &historizing);
+        if (!historizing) {
+            response->results[i].statusCode = UA_STATUSCODE_BADHISTORYOPERATIONINVALID;
+            continue;
+        }
+
+        const UA_HistorizingNodeIdSettings *setting = ctx->gathering.getHistorizingSetting(
+                    server,
+                    ctx->gathering.context,
+                    &nodesToRead[i].nodeId);
+
+        if (!setting) {
+            response->results[i].statusCode = UA_STATUSCODE_BADHISTORYOPERATIONINVALID;
+            continue;
+        }
+
+        if (historyReadDetails->returnBounds && !setting->historizingBackend.boundSupported(
+                    server,
+                    setting->historizingBackend.context,
+                    sessionId,
+                    sessionContext,
+                    &nodesToRead[i].nodeId)) {
+            response->results[i].statusCode = UA_STATUSCODE_BADBOUNDNOTSUPPORTED;
+            continue;
+        }
+
+        if (!setting->historizingBackend.timestampsToReturnSupported(
+                    server,
+                    setting->historizingBackend.context,
+                    sessionId,
+                    sessionContext,
+                    &nodesToRead[i].nodeId,
+                    timestampsToReturn)) {
+            response->results[i].statusCode = UA_STATUSCODE_BADTIMESTAMPNOTSUPPORTED;
+            continue;
+        }
+
+        UA_NumericRange range;
+        range.dimensionsSize = 0;
+        range.dimensions = NULL;
+        if (nodesToRead[i].indexRange.length > 0) {
+            UA_StatusCode rangeParseResult = UA_NumericRange_parseFromString(&range, &nodesToRead[i].indexRange);
+            if (rangeParseResult != UA_STATUSCODE_GOOD) {
+                response->results[i].statusCode = rangeParseResult;
+                continue;
+            }
+        }
+
+        UA_StatusCode getHistoryDataStatusCode;
+        if (setting->historizingBackend.getHistoryData) {
+            getHistoryDataStatusCode = setting->historizingBackend.getHistoryData(
+                        server,
+                        sessionId,
+                        sessionContext,
+                        &setting->historizingBackend,
+                        historyReadDetails->startTime,
+                        historyReadDetails->endTime,
+                        &nodesToRead[i].nodeId,
+                        setting->maxHistoryDataResponseSize,
+                        historyReadDetails->numValuesPerNode,
+                        historyReadDetails->returnBounds,
+                        timestampsToReturn,
+                        range,
+                        releaseContinuationPoints,
+                        &nodesToRead[i].continuationPoint,
+                        &response->results[i].continuationPoint,
+                        historyData[i]);
+        } else {
+            getHistoryDataStatusCode = getHistoryData_service_default(
+                        &setting->historizingBackend,
+                        historyReadDetails->startTime,
+                        historyReadDetails->endTime,
+                        server,
+                        sessionId,
+                        sessionContext,
+                        &nodesToRead[i].nodeId,
+                        setting->maxHistoryDataResponseSize,
+                        historyReadDetails->numValuesPerNode,
+                        historyReadDetails->returnBounds,
+                        timestampsToReturn,
+                        range,
+                        releaseContinuationPoints,
+                        &nodesToRead[i].continuationPoint,
+                        &response->results[i].continuationPoint,
+                        &historyData[i]->dataValuesSize,
+                        &historyData[i]->dataValues);
+        }
+        if (getHistoryDataStatusCode != UA_STATUSCODE_GOOD) {
+            response->results[i].statusCode = getHistoryDataStatusCode;
+            continue;
+        }
+    }
+    response->responseHeader.serviceResult = UA_STATUSCODE_GOOD;
+    return;
+}
+
+static void
+setValue_service_default(UA_Server *server,
+                         void *context,
+                         const UA_NodeId *sessionId,
+                         void *sessionContext,
+                         const UA_NodeId *nodeId,
+                         UA_Boolean historizing,
+                         const UA_DataValue *value)
+{
+    UA_HistoryDatabaseContext_default *ctx = (UA_HistoryDatabaseContext_default*)context;
+    if (ctx->gathering.setValue)
+        ctx->gathering.setValue(server,
+                                ctx->gathering.context,
+                                sessionId,
+                                sessionContext,
+                                nodeId,
+                                historizing,
+                                value);
+}
+
+static void
+deleteMembers_service_default(UA_HistoryDatabase *hdb)
+{
+    if (hdb == NULL || hdb->context == NULL)
+        return;
+    UA_HistoryDatabaseContext_default *ctx = (UA_HistoryDatabaseContext_default*)hdb->context;
+    ctx->gathering.deleteMembers(&ctx->gathering);
+    UA_free(ctx);
+}
+
+UA_HistoryDatabase
+UA_HistoryDatabase_default(UA_HistoryDataGathering gathering)
+{
+    UA_HistoryDatabase hdb;
+    UA_HistoryDatabaseContext_default *context =
+            (UA_HistoryDatabaseContext_default*)
+            UA_calloc(1, sizeof(UA_HistoryDatabaseContext_default));
+    context->gathering = gathering;
+    hdb.context = context;
+    hdb.readRaw = &readRaw_service_default;
+    hdb.setValue = &setValue_service_default;
+    hdb.deleteMembers = &deleteMembers_service_default;
+    return hdb;
+}

+ 26 - 0
plugins/historydata/ua_historydatabase_default.h

@@ -0,0 +1,26 @@
+/* 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 2018 (c) basysKom GmbH <opensource@basyskom.com> (Author: Peter Rustler)
+ */
+
+#ifndef UA_HISTORYDATASERVICE_DEFAULT_H_
+#define UA_HISTORYDATASERVICE_DEFAULT_H_
+
+#include "ua_server.h"
+#include "ua_plugin_historydatabase.h"
+#include "ua_plugin_history_data_gathering.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+UA_HistoryDatabase UA_EXPORT
+UA_HistoryDatabase_default(UA_HistoryDataGathering gathering);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* UA_HISTORYDATASERVICE_DEFAULT_H_ */

+ 228 - 0
plugins/historydata/ua_historydatagathering_default.c

@@ -0,0 +1,228 @@
+/* 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 2018 (c) basysKom GmbH <opensource@basyskom.com> (Author: Peter Rustler)
+ */
+
+#include "ua_historydatagathering_default.h"
+#include "ua_server.h"
+#include "ua_client_subscriptions.h"
+#include <string.h>
+
+typedef struct {
+    UA_NodeId nodeId;
+    UA_HistorizingNodeIdSettings setting;
+    UA_MonitoredItemCreateResult monitoredResult;
+} UA_NodeIdStoreContextItem_gathering_default;
+
+typedef struct {
+    UA_NodeIdStoreContextItem_gathering_default *dataStore;
+    size_t storeEnd;
+    size_t storeSize;
+} UA_NodeIdStoreContext;
+
+static void
+dataChangeCallback_gathering_default(UA_Server *server,
+                                     UA_UInt32 monitoredItemId,
+                                     void *monitoredItemContext,
+                                     const UA_NodeId *nodeId,
+                                     void *nodeContext,
+                                     UA_UInt32 attributeId,
+                                     const UA_DataValue *value)
+{
+    UA_NodeIdStoreContextItem_gathering_default *context = (UA_NodeIdStoreContextItem_gathering_default*)monitoredItemContext;
+    context->setting.historizingBackend.serverSetHistoryData(server,
+                                                             context->setting.historizingBackend.context,
+                                                             NULL,
+                                                             NULL,
+                                                             nodeId,
+                                                             UA_TRUE,
+                                                             value);
+}
+
+static UA_NodeIdStoreContextItem_gathering_default*
+getNodeIdStoreContextItem_gathering_default(UA_NodeIdStoreContext *context,
+                                            const UA_NodeId *nodeId)
+{
+    for (size_t i = 0; i < context->storeEnd; ++i) {
+        if (UA_NodeId_equal(&context->dataStore[i].nodeId, nodeId)) {
+            return &context->dataStore[i];
+        }
+    }
+    return NULL;
+}
+
+static UA_StatusCode
+startPoll(UA_Server *server, UA_NodeIdStoreContextItem_gathering_default *item)
+{
+    UA_MonitoredItemCreateRequest monitorRequest =
+            UA_MonitoredItemCreateRequest_default(item->nodeId);
+    monitorRequest.requestedParameters.samplingInterval = (double)item->setting.pollingInterval;
+    monitorRequest.monitoringMode = UA_MONITORINGMODE_REPORTING;
+    item->monitoredResult =
+            UA_Server_createDataChangeMonitoredItem(server,
+                                                    UA_TIMESTAMPSTORETURN_BOTH,
+                                                    monitorRequest,
+                                                    item,
+                                                    &dataChangeCallback_gathering_default);
+    return item->monitoredResult.statusCode;
+}
+
+static UA_StatusCode
+stopPoll(UA_Server *server, UA_NodeIdStoreContextItem_gathering_default *item)
+{
+    UA_StatusCode retval = UA_Server_deleteMonitoredItem(server, item->monitoredResult.monitoredItemId);
+    UA_MonitoredItemCreateResult_init(&item->monitoredResult);
+    return retval;
+}
+
+static UA_StatusCode
+stopPoll_gathering_default(UA_Server *server,
+                           void *context,
+                           const UA_NodeId *nodeId)
+{
+    UA_NodeIdStoreContext *ctx = (UA_NodeIdStoreContext *)context;
+    UA_NodeIdStoreContextItem_gathering_default *item = getNodeIdStoreContextItem_gathering_default(ctx, nodeId);
+    if (!item) {
+        return UA_STATUSCODE_BADNODEIDUNKNOWN;
+    }
+    if (item->setting.historizingUpdateStrategy != UA_HISTORIZINGUPDATESTRATEGY_POLL)
+        return UA_STATUSCODE_BADNODEIDINVALID;
+    if (item->monitoredResult.monitoredItemId == 0)
+        return UA_STATUSCODE_BADMONITOREDITEMIDINVALID;
+    return stopPoll(server, item);
+}
+
+static UA_StatusCode
+startPoll_gathering_default(UA_Server *server,
+                            void *context,
+                            const UA_NodeId *nodeId)
+{
+    UA_NodeIdStoreContext *ctx = (UA_NodeIdStoreContext *)context;
+    UA_NodeIdStoreContextItem_gathering_default *item = getNodeIdStoreContextItem_gathering_default(ctx, nodeId);
+    if (!item) {
+        return UA_STATUSCODE_BADNODEIDUNKNOWN;
+    }
+    if (item->setting.historizingUpdateStrategy != UA_HISTORIZINGUPDATESTRATEGY_POLL)
+        return UA_STATUSCODE_BADNODEIDINVALID;
+    if (item->monitoredResult.monitoredItemId > 0)
+        return UA_STATUSCODE_BADMONITOREDITEMIDINVALID;
+    return startPoll(server, item);
+}
+
+static UA_StatusCode
+registerNodeId_gathering_default(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) {
+        size_t newStoreSize = ctx->storeSize * 2;
+        ctx->dataStore = (UA_NodeIdStoreContextItem_gathering_default*)UA_realloc(ctx->dataStore,  (newStoreSize * sizeof(UA_NodeIdStoreContextItem_gathering_default)));
+        if (!ctx->dataStore) {
+            ctx->storeSize = 0;
+            return UA_STATUSCODE_BADOUTOFMEMORY;
+        }
+        ctx->storeSize = newStoreSize;
+    }
+    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;
+}
+
+static const UA_HistorizingNodeIdSettings*
+getHistorizingSetting_gathering_default(UA_Server *server,
+                                        void *context,
+                                        const UA_NodeId *nodeId)
+{
+    UA_NodeIdStoreContext *ctx = (UA_NodeIdStoreContext*)context;
+    UA_NodeIdStoreContextItem_gathering_default *item = getNodeIdStoreContextItem_gathering_default(ctx, nodeId);
+    if (item) {
+        return &item->setting;
+    }
+    return NULL;
+}
+
+static void
+deleteMembers_gathering_default(UA_HistoryDataGathering *gathering)
+{
+    if (gathering == NULL || gathering->context == NULL)
+        return;
+    UA_NodeIdStoreContext *ctx = (UA_NodeIdStoreContext*)gathering->context;
+    for (size_t i = 0; i < ctx->storeEnd; ++i) {
+        UA_NodeId_deleteMembers(&ctx->dataStore[i].nodeId);
+        // There is still a monitored item present for this gathering
+        // You need to remove it with UA_Server_deleteMonitoredItem
+        UA_assert(ctx->dataStore[i].monitoredResult.monitoredItemId == 0);
+    }
+    UA_free(ctx->dataStore);
+    UA_free(gathering->context);
+}
+
+static UA_Boolean
+updateNodeIdSetting_gathering_default(UA_Server *server,
+                                      void *context,
+                                      const UA_NodeId *nodeId,
+                                      const UA_HistorizingNodeIdSettings setting)
+{
+    UA_NodeIdStoreContext *ctx = (UA_NodeIdStoreContext*)context;
+    UA_NodeIdStoreContextItem_gathering_default *item = getNodeIdStoreContextItem_gathering_default(ctx, nodeId);
+    if (!item) {
+        return false;
+    }
+    stopPoll_gathering_default(server, context, nodeId);
+    item->setting = setting;
+    return true;
+}
+
+static void
+setValue_gathering_default(UA_Server *server,
+                           void *context,
+                           const UA_NodeId *sessionId,
+                           void *sessionContext,
+                           const UA_NodeId *nodeId,
+                           UA_Boolean historizing,
+                           const UA_DataValue *value)
+{
+    UA_NodeIdStoreContext *ctx = (UA_NodeIdStoreContext*)context;
+    UA_NodeIdStoreContextItem_gathering_default *item = getNodeIdStoreContextItem_gathering_default(ctx, nodeId);
+    if (!item) {
+        return;
+    }
+    if (item->setting.historizingUpdateStrategy == UA_HISTORIZINGUPDATESTRATEGY_VALUESET) {
+        item->setting.historizingBackend.serverSetHistoryData(server,
+                                                              item->setting.historizingBackend.context,
+                                                              sessionId,
+                                                              sessionContext,
+                                                              nodeId,
+                                                              historizing,
+                                                              value);
+    }
+}
+
+UA_HistoryDataGathering
+UA_HistoryDataGathering_Default(size_t initialNodeIdStoreSize)
+{
+    UA_HistoryDataGathering gathering;
+    memset(&gathering, 0, sizeof(UA_HistoryDataGathering));
+    gathering.setValue = &setValue_gathering_default;
+    gathering.getHistorizingSetting = &getHistorizingSetting_gathering_default;
+    gathering.registerNodeId = &registerNodeId_gathering_default;
+    gathering.startPoll = &startPoll_gathering_default;
+    gathering.stopPoll = &stopPoll_gathering_default;
+    gathering.deleteMembers = &deleteMembers_gathering_default;
+    gathering.updateNodeIdSetting = &updateNodeIdSetting_gathering_default;
+    UA_NodeIdStoreContext *context = (UA_NodeIdStoreContext*)UA_calloc(1, sizeof(UA_NodeIdStoreContext));
+    context->storeEnd = 0;
+    context->storeSize = initialNodeIdStoreSize;
+    context->dataStore = (UA_NodeIdStoreContextItem_gathering_default*)UA_calloc(initialNodeIdStoreSize, sizeof(UA_NodeIdStoreContextItem_gathering_default));
+    gathering.context = context;
+    return gathering;
+}

+ 25 - 0
plugins/historydata/ua_historydatagathering_default.h

@@ -0,0 +1,25 @@
+/* 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 2018 (c) basysKom GmbH <opensource@basyskom.com> (Author: Peter Rustler)
+ */
+
+#ifndef UA_HISTORYDATAGATHERING_DEFAULT_H_
+#define UA_HISTORYDATAGATHERING_DEFAULT_H_
+
+#include "ua_plugin_history_data_gathering.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+
+UA_HistoryDataGathering UA_EXPORT
+UA_HistoryDataGathering_Default(size_t initialNodeIdStoreSize);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* UA_HISTORYDATAGATHERING_DEFAULT_H_ */

+ 266 - 0
plugins/historydata/ua_plugin_history_data_backend.h

@@ -0,0 +1,266 @@
+/* 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 2018 (c) basysKom GmbH <opensource@basyskom.com> (Author: Peter Rustler)
+ */
+
+#ifndef UA_PLUGIN_HISTORY_DATA_BACKEND_H_
+#define UA_PLUGIN_HISTORY_DATA_BACKEND_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "ua_types.h"
+#include "ua_server.h"
+
+typedef enum {
+    MATCH_EQUAL,
+    MATCH_AFTER,
+    MATCH_EQUAL_OR_AFTER,
+    MATCH_BEFORE,
+    MATCH_EQUAL_OR_BEFORE
+} MatchStrategy;
+
+typedef struct UA_HistoryDataBackend UA_HistoryDataBackend;
+
+struct UA_HistoryDataBackend {
+    void *context;
+
+    void
+    (*deleteMembers)(UA_HistoryDataBackend *backend);
+
+    /* This function sets a DataValue for a node in the historical data storage.
+     *
+     * server is the server the node lives in.
+     * hdbContext is the context of the UA_HistoryDataBackend.
+     * sessionId and sessionContext identify the session that wants to read historical data.
+     * nodeId is the node for which the value shall be stored.
+     * value is the value which shall be stored.
+     * historizing is the historizing flag of the node identified by nodeId.
+     * If sessionId is NULL, the historizing flag is invalid and must not be used.
+     */
+    UA_StatusCode
+    (*serverSetHistoryData)(UA_Server *server,
+                            void *hdbContext,
+                            const UA_NodeId *sessionId,
+                            void *sessionContext,
+                            const UA_NodeId *nodeId,
+                            UA_Boolean historizing,
+                            const UA_DataValue *value);
+
+    /* This function is the high level interface for the ReadRaw operation.
+     * Set it to NULL if you use the low level API for your plugin.
+     * It should be used if the low level interface does not suite your database.
+     * It is more complex to implement the high level interface but it also provide more freedom.
+     * If you implement this, then set all low level api function pointer to NULL.
+     *
+     * server is the server the node lives in.
+     * hdbContext is the context of the UA_HistoryDataBackend.
+     * sessionId and sessionContext identify the session that wants to read historical data.
+     * backend is the HistoryDataBackend whose storage is to be queried.
+     * start is the start time of the HistoryRead request.
+     * end is the end time of the HistoryRead request.
+     * nodeId is the node id of the node for which historical data is requested.
+     * maxSizePerResponse is the maximum number of items per response the server can provide.
+     * numValuesPerNode is the maximum number of items per response the client wants to receive.
+     * returnBounds determines if the client wants to receive bounding values.
+     * timestampsToReturn contains the time stamps the client is interested in.
+     * range is the numeric range the client wants to read.
+     * releaseContinuationPoints determines if the continuation points shall be released.
+     * continuationPoint is the continuation point the client wants to release or start from.
+     * outContinuationPoint is the continuation point that gets passed to the client by the HistoryRead service.
+     * result contains the result histoy data that gets passed to the client.
+     */
+    UA_StatusCode
+    (*getHistoryData)(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 maxSizePerResponse,
+                      UA_UInt32 numValuesPerNode,
+                      UA_Boolean returnBounds,
+                      UA_TimestampsToReturn timestampsToReturn,
+                      UA_NumericRange range,
+                      UA_Boolean releaseContinuationPoints,
+                      const UA_ByteString *continuationPoint,
+                      UA_ByteString *outContinuationPoint,
+                      UA_HistoryData *result);
+
+    /* This function is part of the low level HistoryRead API.
+     * It returns the index of a value in the database which matches certain criteria.
+     *
+     * server is the server the node lives in.
+     * hdbContext is the context of the UA_HistoryDataBackend.
+     * sessionId and sessionContext identify the session that wants to read historical data.
+     * nodeId is the node id of the node for which the matching value shall be found.
+     * timestamp is the timestamp of the requested index.
+     * strategy is the matching strategy which shall be applied in finding the index.
+     */
+    size_t
+    (*getDateTimeMatch)(UA_Server *server,
+                        void *hdbContext,
+                        const UA_NodeId *sessionId,
+                        void *sessionContext,
+                        const UA_NodeId *nodeId,
+                        const UA_DateTime timestamp,
+                        const MatchStrategy strategy);
+
+    /* This function is part of the low level HistoryRead API.
+     * It returns the index of the element after the last valid entry in the database for a node.
+     *
+     * server is the server the node lives in.
+     * hdbContext is the context of the UA_HistoryDataBackend.
+     * sessionId and sessionContext identify the session that wants to read historical data.
+     * nodeId is the node id of the node for which the end of storage shall be returned.
+     */
+    size_t
+    (*getEnd)(UA_Server *server,
+              void *hdbContext,
+              const UA_NodeId *sessionId,
+              void *sessionContext,
+              const UA_NodeId *nodeId);
+
+    /* This function is part of the low level HistoryRead API.
+     * It returns the index of the last element in the database for a node.
+     *
+     * server is the server the node lives in.
+     * hdbContext is the context of the UA_HistoryDataBackend.
+     * sessionId and sessionContext identify the session that wants to read historical data.
+     * nodeId is the node id of the node for which the index of the last element shall be returned.
+     */
+    size_t
+    (*lastIndex)(UA_Server *server,
+                 void *hdbContext,
+                 const UA_NodeId *sessionId,
+                 void *sessionContext,
+                 const UA_NodeId *nodeId);
+
+    /* This function is part of the low level HistoryRead API.
+     * It returns the index of the first element in the database for a node.
+     *
+     * server is the server the node lives in.
+     * hdbContext is the context of the UA_HistoryDataBackend.
+     * sessionId and sessionContext identify the session that wants to read historical data.
+     * nodeId is the node id of the node for which the index of the first element shall be returned.
+     */
+    size_t
+    (*firstIndex)(UA_Server *server,
+                  void *hdbContext,
+                  const UA_NodeId *sessionId,
+                  void *sessionContext,
+                  const UA_NodeId *nodeId);
+
+    /* This function is part of the low level HistoryRead API.
+     * It returns the number of elements between startIndex and endIndex including both.
+     *
+     * server is the server the node lives in.
+     * hdbContext is the context of the UA_HistoryDataBackend.
+     * sessionId and sessionContext identify the session that wants to read historical data.
+     * nodeId is the node id of the node for which the number of elements shall be returned.
+     * startIndex is the index of the first element in the range.
+     * endIndex is the index of the last element in the range.
+     */
+    size_t
+    (*resultSize)(UA_Server *server,
+                  void *hdbContext,
+                  const UA_NodeId *sessionId,
+                  void *sessionContext,
+                  const UA_NodeId *nodeId,
+                  size_t startIndex,
+                  size_t endIndex);
+
+    /* This function is part of the low level HistoryRead API.
+     * It copies data values inside a certain range into a buffer.
+     *
+     * server is the server the node lives in.
+     * hdbContext is the context of the UA_HistoryDataBackend.
+     * sessionId and sessionContext identify the session that wants to read historical data.
+     * nodeId is the node id of the node for which the data values shall be copied.
+     * startIndex is the index of the first value in the range.
+     * endIndex is the index of the last value in the range.
+     * reverse determines if the values shall be copied in reverse order.
+     * valueSize is the maximal number of data values to copy.
+     * range is the numeric range which shall be copied for every data value.
+     * releaseContinuationPoints determines if the continuation points shall be released.
+     * continuationPoint is a continuation point the client wants to release or start from.
+     * outContinuationPoint is a continuation point which will be passed to the client.
+     * providedValues contains the number of values that were copied.
+     * values contains the values that have been copied from the database.
+     */
+    UA_StatusCode
+    (*copyDataValues)(UA_Server *server,
+                      void *hdbContext,
+                      const UA_NodeId *sessionId,
+                      void *sessionContext,
+                      const UA_NodeId *nodeId,
+                      size_t startIndex,
+                      size_t endIndex,
+                      UA_Boolean reverse,
+                      size_t valueSize,
+                      UA_NumericRange range,
+                      UA_Boolean releaseContinuationPoints,
+                      const UA_ByteString *continuationPoint,
+                      UA_ByteString *outContinuationPoint,
+                      size_t *providedValues,
+                      UA_DataValue *values);
+
+    /* This function is part of the low level HistoryRead API.
+     * It returns the data value stored at a certain index in the database.
+     *
+     * server is the server the node lives in.
+     * hdbContext is the context of the UA_HistoryDataBackend.
+     * sessionId and sessionContext identify the session that wants to read historical data.
+     * nodeId is the node id of the node for which the data value shall be returned.
+     * index is the index in the database for which the data value is requested.
+     */
+    const UA_DataValue*
+    (*getDataValue)(UA_Server *server,
+                    void *hdbContext,
+                    const UA_NodeId *sessionId,
+                    void *sessionContext,
+                    const UA_NodeId *nodeId,
+                    size_t index);
+
+    /* This function returns UA_TRUE if the backend supports returning bounding values for a node.
+     * This function is mandatory.
+     *
+     * server is the server the node lives in.
+     * hdbContext is the context of the UA_HistoryDataBackend.
+     * sessionId and sessionContext identify the session that wants to read historical data.
+     * nodeId is the node id of the node for which the capability to return bounds shall be queried.
+     */
+    UA_Boolean
+    (*boundSupported)(UA_Server *server,
+                      void *hdbContext,
+                      const UA_NodeId *sessionId,
+                      void *sessionContext,
+                      const UA_NodeId *nodeId);
+
+    /* This function returns UA_TRUE if the backend supports returning the requested timestamps for a node.
+     * This function is mandatory.
+     *
+     * server is the server the node lives in.
+     * hdbContext is the context of the UA_HistoryDataBackend.
+     * sessionId and sessionContext identify the session that wants to read historical data.
+     * nodeId is the node id of the node for which the capability to return certain timestamps shall be queried.
+     */
+    UA_Boolean
+    (*timestampsToReturnSupported)(UA_Server *server,
+                                   void *hdbContext,
+                                   const UA_NodeId *sessionId,
+                                   void *sessionContext,
+                                   const UA_NodeId *nodeId,
+                                   const UA_TimestampsToReturn timestampsToReturn);
+
+};
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* UA_PLUGIN_HISTORY_DATA_BACKEND_H_ */

+ 124 - 0
plugins/historydata/ua_plugin_history_data_gathering.h

@@ -0,0 +1,124 @@
+/* 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 2018 (c) basysKom GmbH <opensource@basyskom.com> (Author: Peter Rustler)
+ */
+
+#ifndef UA_PLUGIN_HISTORY_DATA_GATHERING_H_
+#define UA_PLUGIN_HISTORY_DATA_GATHERING_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "ua_types.h"
+#include "ua_server.h"
+#include "ua_plugin_history_data_backend.h"
+
+typedef enum {
+    UA_HISTORIZINGUPDATESTRATEGY_USER     = 0x00,
+    UA_HISTORIZINGUPDATESTRATEGY_VALUESET = 0x01,
+    UA_HISTORIZINGUPDATESTRATEGY_POLL     = 0x02
+} UA_HistorizingUpdateStrategy;
+
+typedef struct {
+    UA_HistoryDataBackend historizingBackend;
+    size_t maxHistoryDataResponseSize;
+    UA_HistorizingUpdateStrategy historizingUpdateStrategy;
+    size_t pollingInterval;
+    void * userContext;
+} UA_HistorizingNodeIdSettings;
+
+struct UA_HistoryDataGathering;
+typedef struct UA_HistoryDataGathering UA_HistoryDataGathering;
+struct UA_HistoryDataGathering {
+    void *context;
+
+    void
+    (*deleteMembers)(UA_HistoryDataGathering *gathering);
+
+    /* This function registers a node for the gathering of historical data.
+     *
+     * server is the server the node lives in.
+     * hdgContext is the context of the UA_HistoryDataGathering.
+     * nodeId is the node id of the node to register.
+     * setting contains the gatering settings for the node to register.
+     */
+    UA_StatusCode
+    (*registerNodeId)(UA_Server *server,
+                      void *hdgContext,
+                      const UA_NodeId *nodeId,
+                      const UA_HistorizingNodeIdSettings setting);
+
+    /* This function stops polling a node for value changes.
+     *
+     * server is the server the node lives in.
+     * hdgContext is the context of the UA_HistoryDataGathering.
+     * nodeId is id of the node for which polling shall be stopped.
+     * setting contains the gatering settings for the node.
+     */
+    UA_StatusCode
+    (*stopPoll)(UA_Server *server,
+                void *hdgContext,
+                const UA_NodeId *nodeId);
+
+    /* This function starts polling a node for value changes.
+     *
+     * server is the server the node lives in.
+     * hdgContext is the context of the UA_HistoryDataGathering.
+     * nodeId is the id of the node for which polling shall be started.
+     */
+    UA_StatusCode
+    (*startPoll)(UA_Server *server,
+                 void *hdgContext,
+                 const UA_NodeId *nodeId);
+
+    /* This function modifies the gathering settings for a node.
+     *
+     * server is the server the node lives in.
+     * hdgContext is the context of the UA_HistoryDataGathering.
+     * nodeId is the node id of the node for which gathering shall be modified.
+     * setting contains the new gatering settings for the node.
+     */
+    UA_Boolean
+    (*updateNodeIdSetting)(UA_Server *server,
+                           void *hdgContext,
+                           const UA_NodeId *nodeId,
+                           const UA_HistorizingNodeIdSettings setting);
+
+    /* Returns the gathering settings for a node.
+     *
+     * server is the server the node lives in.
+     * hdgContext is the context of the UA_HistoryDataGathering.
+     * nodeId is the node id of the node for which the gathering settings shall be retrieved.
+     */
+    const UA_HistorizingNodeIdSettings*
+    (*getHistorizingSetting)(UA_Server *server,
+                             void *hdgContext,
+                             const UA_NodeId *nodeId);
+
+    /* Sets a DataValue for a node in the historical data storage.
+     *
+     * server is the server the node lives in.
+     * hdgContext is the context of the UA_HistoryDataGathering.
+     * sessionId and sessionContext identify the session which wants to set this value.
+     * nodeId is the node id of the node for which a value shall be set.
+     * historizing is the historizing flag of the node identified by nodeId.
+     * value is the value to set in the history data storage.
+     */
+    void
+    (*setValue)(UA_Server *server,
+                void *hdgContext,
+                const UA_NodeId *sessionId,
+                void *sessionContext,
+                const UA_NodeId *nodeId,
+                UA_Boolean historizing,
+                const UA_DataValue *value);
+};
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* UA_PLUGIN_HISTORY_DATA_GATHERING_H_ */

+ 8 - 0
tests/CMakeLists.txt

@@ -13,6 +13,7 @@ include_directories(${PROJECT_SOURCE_DIR}/src)
 include_directories(${PROJECT_SOURCE_DIR}/src/server)
 include_directories(${PROJECT_SOURCE_DIR}/src/pubsub)
 include_directories(${PROJECT_SOURCE_DIR}/plugins)
+include_directories(${PROJECT_SOURCE_DIR}/plugins/historydata)
 include_directories(${PROJECT_BINARY_DIR}/src_generated)
 include_directories(${PROJECT_SOURCE_DIR}/tests/testing-plugins)
 
@@ -39,6 +40,9 @@ set(test_plugin_sources ${PROJECT_SOURCE_DIR}/arch/ua_network_tcp.c
                         ${PROJECT_SOURCE_DIR}/plugins/ua_pki_certificate.c
                         ${PROJECT_SOURCE_DIR}/plugins/ua_nodestore_default.c
                         ${PROJECT_SOURCE_DIR}/tests/testing-plugins/testing_clock.c
+                        ${PROJECT_SOURCE_DIR}/plugins/historydata/ua_historydatabackend_memory.c
+                        ${PROJECT_SOURCE_DIR}/plugins/historydata/ua_historydatagathering_default.c
+                        ${PROJECT_SOURCE_DIR}/plugins/historydata/ua_historydatabase_default.c
                         ${PROJECT_SOURCE_DIR}/plugins/ua_securitypolicy_none.c
                         ${PROJECT_SOURCE_DIR}/tests/testing-plugins/testing_policy.c
                         ${PROJECT_SOURCE_DIR}/tests/testing-plugins/testing_networklayers.c
@@ -153,6 +157,10 @@ add_executable(check_nodestore server/check_nodestore.c $<TARGET_OBJECTS:open625
 target_link_libraries(check_nodestore ${LIBS})
 add_test_valgrind(nodestore ${TESTS_BINARY_DIR}/check_nodestore)
 
+add_executable(check_historical_data server/check_historical_data.c $<TARGET_OBJECTS:open62541-object> $<TARGET_OBJECTS:open62541-testplugins>)
+target_link_libraries(check_historical_data ${LIBS})
+add_test_valgrind(historical_data ${TESTS_BINARY_DIR}/check_historical_data)
+
 add_executable(check_session server/check_session.c $<TARGET_OBJECTS:open62541-object> $<TARGET_OBJECTS:open62541-testplugins>)
 target_link_libraries(check_session ${LIBS})
 add_test_valgrind(session ${TESTS_BINARY_DIR}/check_session)

+ 652 - 0
tests/server/check_historical_data.c

@@ -0,0 +1,652 @@
+/* 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 2018 (c) basysKom GmbH <opensource@basyskom.com> (Author: Peter Rustler)
+ */
+
+#include "ua_types.h"
+#include "ua_server.h"
+#include "server/ua_server_internal.h"
+#include "ua_client.h"
+#include "client/ua_client_internal.h"
+#include "ua_client_highlevel.h"
+#include "ua_config_default.h"
+#include "ua_network_tcp.h"
+
+#include "check.h"
+#include "testing_clock.h"
+#include "testing_networklayers.h"
+#include "thread_wrapper.h"
+#include "ua_plugin_historydatabase.h"
+#include "ua_historydatabase_default.h"
+#include "ua_plugin_history_data_gathering.h"
+#include "ua_historydatabackend_memory.h"
+#include "ua_historydatagathering_default.h"
+#include "historical_read_test_data.h"
+#include <stddef.h>
+
+UA_Server *server;
+UA_ServerConfig *config;
+UA_HistoryDataGathering *gathering;
+UA_Boolean running;
+THREAD_HANDLE server_thread;
+
+UA_Client *client;
+UA_NodeId parentNodeId;
+UA_NodeId parentReferenceNodeId;
+UA_NodeId outNodeId;
+
+THREAD_CALLBACK(serverloop)
+{
+    while(running)
+        UA_Server_run_iterate(server, true);
+    return 0;
+}
+
+static void
+setup(void)
+{
+    running = true;
+    config = UA_ServerConfig_new_default();
+    gathering = (UA_HistoryDataGathering*)UA_calloc(1, sizeof(UA_HistoryDataGathering));
+    *gathering = UA_HistoryDataGathering_Default(1);
+    config->historyDatabase = UA_HistoryDatabase_default(*gathering);
+    server = UA_Server_new(config);
+    UA_StatusCode retval = UA_Server_run_startup(server);
+    ck_assert_str_eq(UA_StatusCode_name(retval), UA_StatusCode_name(UA_STATUSCODE_GOOD));
+    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;
+    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);
+    ck_assert_uint_eq(UA_Server_addVariableNode(server,
+                                                uint32NodeId,
+                                                parentNodeId,
+                                                parentReferenceNodeId,
+                                                uint32Name,
+                                                UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE),
+                                                attr,
+                                                NULL,
+                                                &outNodeId)
+                      , UA_STATUSCODE_GOOD);
+
+    client = UA_Client_new(UA_ClientConfig_default);
+    retval = UA_Client_connect(client, "opc.tcp://localhost:4840");
+    ck_assert_str_eq(UA_StatusCode_name(retval), UA_StatusCode_name(UA_STATUSCODE_GOOD));
+
+    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);
+    UA_NodeId_deleteMembers(&parentNodeId);
+    UA_NodeId_deleteMembers(&parentReferenceNodeId);
+    UA_NodeId_deleteMembers(&outNodeId);
+    running = false;
+    THREAD_JOIN(server_thread);
+    UA_Server_run_shutdown(server);
+    UA_Server_delete(server);
+    UA_ServerConfig_delete(config);
+    UA_free(gathering);
+}
+
+#ifdef UA_ENABLE_HISTORIZING
+
+#include <stdio.h>
+#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);
+}
+
+static void
+printTimestamp(UA_DateTime timestamp)
+{
+    if (timestamp == TIMESTAMP_FIRST) {
+        fprintf(stderr, "FIRST,");
+    } else if (timestamp == TIMESTAMP_LAST) {
+        fprintf(stderr, "LAST,");
+    } else {
+        fprintf(stderr, "%3lld,", timestamp / UA_DATETIME_SEC);
+    }
+}
+
+static void
+printResult(UA_DataValue * value)
+{
+    if (value->status != UA_STATUSCODE_GOOD)
+        fprintf(stderr, "%s:", UA_StatusCode_name(value->status));
+    printTimestamp(value->sourceTimestamp);
+}
+
+static UA_Boolean
+resultIsEqual(const UA_DataValue * result, const testTuple * tuple, size_t index)
+{
+    switch (tuple->result[index]) {
+    case TIMESTAMP_FIRST:
+        if (result->status != UA_STATUSCODE_BADBOUNDNOTFOUND
+                || !UA_Variant_isEmpty(&result->value))
+            return false;
+        /* we do not test timestamp if TIMESTAMP_UNSPECIFIED is given for start.
+         * See OPC UA Part 11, Version 1.03, Page 5-6, Table 1, Mark b for details.*/
+        if (tuple->start != TIMESTAMP_UNSPECIFIED
+                && tuple->start != result->sourceTimestamp)
+            return false;
+        break;
+    case TIMESTAMP_LAST:
+        if (result->status != UA_STATUSCODE_BADBOUNDNOTFOUND
+                || !UA_Variant_isEmpty(&result->value))
+            return false;
+        /* we do not test timestamp if TIMESTAMP_UNSPECIFIED is given for end.
+         * See OPC UA Part 11, Version 1.03, Page 5-6, Table 1, Mark a for details.*/
+        if (tuple->end != TIMESTAMP_UNSPECIFIED
+                && tuple->end != result->sourceTimestamp)
+            return false;
+        break;
+    default:
+        if (result->sourceTimestamp != tuple->result[index]
+                || result->value.type != &UA_TYPES[UA_TYPES_INT64]
+                || *((UA_Int64*)result->value.data) != tuple->result[index])
+            return false;
+    }
+    return true;
+}
+
+static UA_Boolean
+fillHistoricalDataBackend(UA_HistoryDataBackend backend)
+{
+    int i = 0;
+    UA_DateTime currentDateTime = testData[i];
+    fprintf(stderr, "Adding to historical data backend: ");
+    while (currentDateTime) {
+        fprintf(stderr, "%lld, ", currentDateTime / UA_DATETIME_SEC);
+        UA_DataValue value;
+        UA_DataValue_init(&value);
+        value.hasValue = true;
+        UA_Int64 d = currentDateTime;
+        UA_Variant_setScalarCopy(&value.value, &d, &UA_TYPES[UA_TYPES_INT64]);
+        value.hasSourceTimestamp = true;
+        value.sourceTimestamp = currentDateTime;
+        value.hasStatus = true;
+        value.status = UA_STATUSCODE_GOOD;
+        if (backend.serverSetHistoryData(server, backend.context, NULL, NULL, &outNodeId, UA_FALSE, &value) != UA_STATUSCODE_GOOD) {
+            fprintf(stderr, "\n");
+            return false;
+        }
+        UA_DataValue_deleteMembers(&value);
+        currentDateTime = testData[++i];
+    }
+    fprintf(stderr, "\n");
+    return true;
+}
+
+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_SOURCE;
+
+    request.nodesToReadSize = 1;
+    request.nodesToRead = valueId;
+
+    Service_HistoryRead(server, &server->adminSession, &request, response);
+    UA_HistoryReadRequest_deleteMembers(&request);
+}
+
+static UA_UInt32
+testHistoricalDataBackend(size_t maxResponseSize)
+{
+    const UA_HistorizingNodeIdSettings* setting = gathering->getHistorizingSetting(server, gathering->context, &outNodeId);
+    UA_HistorizingNodeIdSettings newSetting = *setting;
+    newSetting.maxHistoryDataResponseSize = maxResponseSize;
+    gathering->updateNodeIdSetting(server, gathering->context, &outNodeId, newSetting);
+
+    UA_UInt32 retval = 0;
+    size_t i = 0;
+    testTuple *current = &testRequests[i];
+    fprintf(stderr, "Testing with maxResponseSize of %lu\n", maxResponseSize);
+    fprintf(stderr, "Start | End  | numValuesPerNode | returnBounds |ContPoint| {Expected}{Result} Result\n");
+    fprintf(stderr, "------+------+------------------+--------------+---------+----------------\n");
+    size_t j;
+    while (current->start || current->end) {
+        j = 0;
+        if (current->start == TIMESTAMP_UNSPECIFIED) {
+            fprintf(stderr, "UNSPEC|");
+        } else {
+            fprintf(stderr, "  %3lld |", current->start / UA_DATETIME_SEC);
+        }
+        if (current->end == TIMESTAMP_UNSPECIFIED) {
+            fprintf(stderr, "UNSPEC|");
+        } else {
+            fprintf(stderr, "  %3lld |", current->end / UA_DATETIME_SEC);
+        }
+        fprintf(stderr, "               %2u |          %s |     %s | {", current->numValuesPerNode, (current->returnBounds ? "Yes" : " No"), (current->returnContinuationPoint ? "Yes" : " No"));
+        while (current->result[j]) {
+            printTimestamp(current->result[j]);
+            ++j;
+        }
+        fprintf(stderr, "}");
+
+        UA_DataValue *result = NULL;
+        size_t resultSize = 0;
+        UA_ByteString continuous;
+        UA_ByteString_init(&continuous);
+        UA_Boolean readOk = true;
+        size_t reseivedValues = 0;
+        fprintf(stderr, "{");
+        size_t counter = 0;
+        do {
+            UA_HistoryReadResponse response;
+            UA_HistoryReadResponse_init(&response);
+            UA_UInt32 numValuesPerNode = current->numValuesPerNode;
+            if (numValuesPerNode > 0 && numValuesPerNode + (UA_UInt32)reseivedValues > current->numValuesPerNode)
+                numValuesPerNode = current->numValuesPerNode - (UA_UInt32)reseivedValues;
+
+            requestHistory(current->start,
+                           current->end,
+                           &response,
+                           numValuesPerNode,
+                           current->returnBounds,
+                           &continuous);
+            ++counter;
+
+            if(response.resultsSize != 1) {
+                fprintf(stderr, "ResultError:Size %lu %s", response.resultsSize, UA_StatusCode_name(response.responseHeader.serviceResult));
+                readOk = false;
+                UA_HistoryReadResponse_deleteMembers(&response);
+                break;
+            }
+
+            UA_StatusCode stat = response.results[0].statusCode;
+            if (stat == UA_STATUSCODE_BADBOUNDNOTSUPPORTED && current->returnBounds) {
+                fprintf(stderr, "%s", UA_StatusCode_name(stat));
+                UA_HistoryReadResponse_deleteMembers(&response);
+                break;
+            }
+
+            if(response.results[0].historyData.encoding != UA_EXTENSIONOBJECT_DECODED
+                    || response.results[0].historyData.content.decoded.type != &UA_TYPES[UA_TYPES_HISTORYDATA]) {
+                fprintf(stderr, "ResultError:HistoryData");
+                readOk = false;
+                UA_HistoryReadResponse_deleteMembers(&response);
+                break;
+            }
+
+            UA_HistoryData * data = (UA_HistoryData *)response.results[0].historyData.content.decoded.data;
+            resultSize = data->dataValuesSize;
+            result = data->dataValues;
+
+            if (resultSize == 0 && continuous.length > 0) {
+                fprintf(stderr, "continuousResultEmpty");
+                readOk = false;
+                UA_HistoryReadResponse_deleteMembers(&response);
+                break;
+            }
+
+            if (resultSize > maxResponseSize) {
+                fprintf(stderr, "resultToBig");
+                readOk = false;
+                UA_HistoryReadResponse_deleteMembers(&response);
+                break;
+            }
+
+            if (stat != UA_STATUSCODE_GOOD) {
+                fprintf(stderr, "%s", UA_StatusCode_name(stat));
+            } else {
+                for (size_t k = 0; k < resultSize; ++k)
+                    printResult(&result[k]);
+            }
+
+            if (stat == UA_STATUSCODE_GOOD && j >= resultSize + reseivedValues) {
+                for (size_t l = 0; l < resultSize; ++l) {
+                    /* See OPC UA Part 11, Version 1.03, Page 5-6, Table 1, Mark a for details.*/
+                    if (current->result[l + reseivedValues] == TIMESTAMP_LAST && current->end == TIMESTAMP_UNSPECIFIED) {
+                        // This test will work on not continous read, only
+                        if (reseivedValues == 0 && !(l > 0 && result[l].sourceTimestamp == result[l-1].sourceTimestamp + UA_DATETIME_SEC))
+                            readOk = false;
+                    }
+                    /* See OPC UA Part 11, Version 1.03, Page 5-6, Table 1, Mark b for details.*/
+                    if (current->result[l + reseivedValues] == TIMESTAMP_FIRST && current->start == TIMESTAMP_UNSPECIFIED) {
+                        // This test will work on not continous read, only
+                        if (reseivedValues == 0 && !(l > 0 && result[l].sourceTimestamp == result[l-1].sourceTimestamp - UA_DATETIME_SEC))
+                            readOk = false;
+                    }
+                    if (!resultIsEqual(&result[l], current, l + reseivedValues))
+                        readOk = false;
+                }
+                if (response.results[0].continuationPoint.length > 0)
+                    fprintf(stderr, "C,");
+                reseivedValues += resultSize;
+                if (reseivedValues == j) {
+                    if (current->returnContinuationPoint && response.results[0].continuationPoint.length == 0) {
+                        readOk = false;
+                        fprintf(stderr, "missingContinuationPoint");
+                    }
+                    if (!current->returnContinuationPoint && response.results[0].continuationPoint.length > 0) {
+                        readOk = false;
+                        fprintf(stderr, "unexpectedContinuationPoint");
+                    }
+                    UA_HistoryReadResponse_deleteMembers(&response);
+                    break;
+                }
+                UA_ByteString_deleteMembers(&continuous);
+                UA_ByteString_copy(&response.results[0].continuationPoint, &continuous);
+            } else {
+                readOk = false;
+                UA_HistoryReadResponse_deleteMembers(&response);
+                break;
+            }
+            UA_HistoryReadResponse_deleteMembers(&response);
+        } while (continuous.length > 0);
+
+        if (j != reseivedValues) {
+            readOk = false;
+        }
+        UA_ByteString_deleteMembers(&continuous);
+        if (!readOk) {
+            fprintf(stderr, "} Fail (%lu requests)\n", counter);
+            ++retval;
+        } else {
+            fprintf(stderr, "} OK (%lu requests)\n", counter);
+        }
+        current = &testRequests[++i];
+    }
+    return retval;
+}
+
+START_TEST(Server_HistorizingStrategyUser)
+{
+    // set a data backend
+    UA_HistorizingNodeIdSettings setting;
+    setting.historizingBackend = UA_HistoryDataBackend_Memory(3, 100);
+    setting.maxHistoryDataResponseSize = 100;
+    setting.historizingUpdateStrategy = UA_HISTORIZINGUPDATESTRATEGY_USER;
+    UA_StatusCode retval = gathering->registerNodeId(server, gathering->context, &outNodeId, setting);
+    ck_assert_str_eq(UA_StatusCode_name(retval), UA_StatusCode_name(UA_STATUSCODE_GOOD));
+
+    // fill the data
+    UA_DateTime start = UA_DateTime_now();
+    UA_DateTime end = start + (10 * UA_DATETIME_SEC);
+    for (UA_UInt32 i = 0; i < 10; ++i) {
+        UA_DataValue value;
+        UA_DataValue_init(&value);
+        value.hasValue = true;
+        value.hasStatus = true;
+        value.status = UA_STATUSCODE_GOOD;
+        UA_Variant_setScalarCopy(&value.value, &i, &UA_TYPES[UA_TYPES_UINT32]);
+        value.hasSourceTimestamp = true;
+        value.sourceTimestamp = start + (i * UA_DATETIME_SEC);
+        retval = setting.historizingBackend.serverSetHistoryData(server,
+                                                                 setting.historizingBackend.context,
+                                                                 NULL,
+                                                                 NULL,
+                                                                 &outNodeId,
+                                                                 UA_FALSE,
+                                                                 &value);
+        ck_assert_str_eq(UA_StatusCode_name(retval), UA_StatusCode_name(UA_STATUSCODE_GOOD));
+        UA_DataValue_deleteMembers(&value);
+    }
+
+    // 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_uint_eq(data->dataValuesSize, 10);
+        for (size_t j = 0; j < data->dataValuesSize; ++j) {
+            ck_assert_uint_eq(data->dataValues[j].hasSourceTimestamp, true);
+            ck_assert_uint_eq(data->dataValues[j].sourceTimestamp, start + (j * UA_DATETIME_SEC));
+            ck_assert_uint_eq(data->dataValues[j].hasStatus, 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;
+            ck_assert_uint_eq(*value, j);
+        }
+    }
+    UA_HistoryReadResponse_deleteMembers(&response);
+    UA_HistoryDataBackend_Memory_deleteMembers(&setting.historizingBackend);
+}
+END_TEST
+
+START_TEST(Server_HistorizingStrategyPoll)
+{
+    // 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(3, 100);
+    setting.maxHistoryDataResponseSize = 100;
+    setting.pollingInterval = 100;
+    setting.historizingUpdateStrategy = UA_HISTORIZINGUPDATESTRATEGY_POLL;
+    retval = gathering->registerNodeId(server, gathering->context, &outNodeId, setting);
+    ck_assert_str_eq(UA_StatusCode_name(retval), UA_StatusCode_name(UA_STATUSCODE_GOOD));
+
+    // fill the data
+    UA_DateTime start = UA_DateTime_now();
+    retval = gathering->startPoll(server, gathering->context, &outNodeId);
+    ck_assert_str_eq(UA_StatusCode_name(retval), UA_StatusCode_name(UA_STATUSCODE_GOOD));
+
+    ck_assert_str_eq(UA_StatusCode_name(retval), UA_StatusCode_name(UA_STATUSCODE_GOOD));
+    for (size_t k = 0; k < 10; ++k) {
+        UA_fakeSleep(50);
+        UA_realSleep(50);
+        if (k == 5) {
+            gathering->stopPoll(server, gathering->context, &outNodeId);
+        }
+        setUInt32(client, outNodeId, (unsigned int)k);
+    }
+
+    ck_assert_str_eq(UA_StatusCode_name(retval), UA_StatusCode_name(UA_STATUSCODE_GOOD));
+    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 > 1);
+        for (size_t j = 0; j < data->dataValuesSize; ++j) {
+            ck_assert_uint_eq(data->dataValues[j].hasSourceTimestamp, true);
+            ck_assert(data->dataValues[j].sourceTimestamp >= start);
+            ck_assert(data->dataValues[j].sourceTimestamp < end);
+            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;
+            // first need to be 43
+            if (j == 0) {
+                ck_assert(*value == 43);
+            } else {
+                ck_assert(*value < 5);
+            }
+        }
+    }
+    UA_HistoryReadResponse_deleteMembers(&response);
+    UA_HistoryDataBackend_Memory_deleteMembers(&setting.historizingBackend);
+}
+END_TEST
+
+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(3, 100);
+    setting.maxHistoryDataResponseSize = 100;
+    setting.historizingUpdateStrategy = UA_HISTORIZINGUPDATESTRATEGY_VALUESET;
+    retval = gathering->registerNodeId(server, gathering->context, &outNodeId, setting);
+    ck_assert_str_eq(UA_StatusCode_name(retval), UA_StatusCode_name(UA_STATUSCODE_GOOD));
+
+    // fill the data
+    UA_fakeSleep(100);
+    UA_DateTime start = UA_DateTime_now();
+    UA_fakeSleep(100);
+    for (UA_UInt32 i = 0; i < 10; ++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;
+            ck_assert_uint_eq(*value, j);
+        }
+    }
+    UA_HistoryReadResponse_deleteMembers(&response);
+    UA_HistoryDataBackend_Memory_deleteMembers(&setting.historizingBackend);
+}
+END_TEST
+
+START_TEST(Server_HistorizingBackendMemory)
+{
+    UA_HistoryDataBackend backend = UA_HistoryDataBackend_Memory(1, 1);
+    UA_HistorizingNodeIdSettings setting;
+    setting.historizingBackend = backend;
+    setting.maxHistoryDataResponseSize = 1000;
+    setting.historizingUpdateStrategy = UA_HISTORIZINGUPDATESTRATEGY_USER;
+    UA_StatusCode ret = gathering->registerNodeId(server, gathering->context, &outNodeId, setting);
+    ck_assert_str_eq(UA_StatusCode_name(ret), UA_StatusCode_name(UA_STATUSCODE_GOOD));
+
+    // empty backend should not crash
+    UA_UInt32 retval = testHistoricalDataBackend(100);
+    fprintf(stderr, "%d tests expected failed.\n", retval);
+
+    // fill backend
+    ck_assert_uint_eq(fillHistoricalDataBackend(backend), true);
+
+    // read all in one
+    retval = testHistoricalDataBackend(100);
+    fprintf(stderr, "%d tests failed.\n", retval);
+    ck_assert_uint_eq(retval, 0);
+
+    // read continuous one at one request
+    retval = testHistoricalDataBackend(1);
+    fprintf(stderr, "%d tests failed.\n", retval);
+    ck_assert_uint_eq(retval, 0);
+
+    // read continuous two at one request
+    retval = testHistoricalDataBackend(2);
+    fprintf(stderr, "%d tests failed.\n", retval);
+    ck_assert_uint_eq(retval, 0);
+    UA_HistoryDataBackend_Memory_deleteMembers(&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 Basic");
+    tcase_add_checked_fixture(tc_server, setup, teardown);
+#ifdef UA_ENABLE_HISTORIZING
+    tcase_add_test(tc_server, Server_HistorizingStrategyPoll);
+    tcase_add_test(tc_server, Server_HistorizingStrategyUser);
+    tcase_add_test(tc_server, Server_HistorizingStrategyValueSet);
+    tcase_add_test(tc_server, Server_HistorizingBackendMemory);
+#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;
+}

+ 397 - 0
tests/server/historical_read_test_data.h

@@ -0,0 +1,397 @@
+/* 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 2018 (c) basysKom GmbH <opensource@basyskom.com> (Author: Peter Rustler)
+ */
+
+/* Data in this header is gathered from OPC Unified Architecture, Part 11, Release
+1.03 Page 5-6 from OPC Foundation */
+
+#ifndef UA_HISTORICAL_READ_TEST_DATA_H_
+#define UA_HISTORICAL_READ_TEST_DATA_H_
+
+#include "ua_types.h"
+#include <limits.h>
+
+typedef struct {
+    UA_DateTime start;
+    UA_DateTime end;
+    UA_UInt32 numValuesPerNode;
+    UA_Boolean returnBounds;
+    UA_DateTime result[8];
+    UA_Boolean returnContinuationPoint;
+} testTuple;
+
+#define TIMESTAMP_UNSPECIFIED LLONG_MIN
+#define NODATA 0
+#define TIMESTAMP_FIRST 1
+#define TIMESTAMP_4_48 (448 * UA_DATETIME_SEC)
+#define TIMESTAMP_4_58 (458 * UA_DATETIME_SEC)
+#define TIMESTAMP_4_59 (459 * UA_DATETIME_SEC)
+#define TIMESTAMP_5_00 (500 * UA_DATETIME_SEC)
+#define TIMESTAMP_5_01 (501 * UA_DATETIME_SEC)
+#define TIMESTAMP_5_02 (502 * UA_DATETIME_SEC)
+#define TIMESTAMP_5_03 (503 * UA_DATETIME_SEC)
+#define TIMESTAMP_5_04 (504 * UA_DATETIME_SEC)
+#define TIMESTAMP_5_05 (505 * UA_DATETIME_SEC)
+#define TIMESTAMP_5_06 (506 * UA_DATETIME_SEC)
+#define TIMESTAMP_5_07 (507 * UA_DATETIME_SEC)
+#define TIMESTAMP_LAST (600 * UA_DATETIME_SEC)
+
+static UA_DateTime testData[] = {
+    TIMESTAMP_5_03,
+    TIMESTAMP_5_00,
+    TIMESTAMP_5_02,
+    TIMESTAMP_5_06,
+    TIMESTAMP_5_05,
+    0 // last element
+};
+static testTuple testRequests[] =
+{
+    { TIMESTAMP_5_00,
+      TIMESTAMP_5_05,
+      0,
+      true,
+      { TIMESTAMP_5_00, TIMESTAMP_5_02, TIMESTAMP_5_03, TIMESTAMP_5_05, 0 },
+      false
+    },
+    { TIMESTAMP_5_00,
+      TIMESTAMP_5_05,
+      0,
+      false,
+      { TIMESTAMP_5_00, TIMESTAMP_5_02, TIMESTAMP_5_03, 0 },
+      false
+    },
+    { TIMESTAMP_5_01,
+      TIMESTAMP_5_04,
+      0,
+      true,
+      { TIMESTAMP_5_00, TIMESTAMP_5_02, TIMESTAMP_5_03, TIMESTAMP_5_05, 0 },
+      false
+    },
+    { TIMESTAMP_5_01,
+      TIMESTAMP_5_04,
+      0,
+      false,
+      { TIMESTAMP_5_02, TIMESTAMP_5_03, 0 },
+      false
+    },
+    { TIMESTAMP_5_05,
+      TIMESTAMP_5_00,
+      0,
+      true,
+      { TIMESTAMP_5_05, TIMESTAMP_5_03, TIMESTAMP_5_02, TIMESTAMP_5_00, 0 },
+      false
+    },
+    { TIMESTAMP_5_05,
+      TIMESTAMP_5_00,
+      0,
+      false,
+      { TIMESTAMP_5_05, TIMESTAMP_5_03, TIMESTAMP_5_02, 0 },
+      false
+    },
+    { TIMESTAMP_5_04,
+      TIMESTAMP_5_01,
+      0,
+      true,
+      { TIMESTAMP_5_05, TIMESTAMP_5_03, TIMESTAMP_5_02, TIMESTAMP_5_00, 0 },
+      false
+    },
+    { TIMESTAMP_5_04,
+      TIMESTAMP_5_01,
+      0,
+      false,
+      { TIMESTAMP_5_03, TIMESTAMP_5_02, 0 },
+      false
+    },
+    { TIMESTAMP_4_59,
+      TIMESTAMP_5_05,
+      0,
+      true,
+      { TIMESTAMP_FIRST, TIMESTAMP_5_00, TIMESTAMP_5_02, TIMESTAMP_5_03, TIMESTAMP_5_05, 0 },
+      false
+    },
+    { TIMESTAMP_4_59,
+      TIMESTAMP_5_05,
+      0,
+      false,
+      { TIMESTAMP_5_00, TIMESTAMP_5_02, TIMESTAMP_5_03, 0 },
+      false
+    },
+    { TIMESTAMP_5_01,
+      TIMESTAMP_5_07,
+      0,
+      true,
+      { TIMESTAMP_5_00, TIMESTAMP_5_02, TIMESTAMP_5_03, TIMESTAMP_5_05, TIMESTAMP_5_06, TIMESTAMP_LAST, 0 },
+      false
+    },
+    { TIMESTAMP_5_01,
+      TIMESTAMP_5_07,
+      0,
+      false,
+      { TIMESTAMP_5_02, TIMESTAMP_5_03, TIMESTAMP_5_05, TIMESTAMP_5_06, 0 },
+      false
+    },
+    { TIMESTAMP_5_00,
+      TIMESTAMP_5_05,
+      3,
+      true,
+      { TIMESTAMP_5_00, TIMESTAMP_5_02, TIMESTAMP_5_03, 0 },
+      true
+    },
+    { TIMESTAMP_5_00,
+      TIMESTAMP_5_05,
+      3,
+      false,
+      { TIMESTAMP_5_00, TIMESTAMP_5_02, TIMESTAMP_5_03, 0 },
+      false
+    },
+    { TIMESTAMP_5_01,
+      TIMESTAMP_5_04,
+      3,
+      true,
+      { TIMESTAMP_5_00, TIMESTAMP_5_02, TIMESTAMP_5_03, 0 },
+      true
+    },
+    { TIMESTAMP_5_01,
+      TIMESTAMP_5_04,
+      3,
+      false,
+      { TIMESTAMP_5_02, TIMESTAMP_5_03, 0 },
+      false
+    },
+    { TIMESTAMP_5_05,
+      TIMESTAMP_5_00,
+      3,
+      true,
+      { TIMESTAMP_5_05, TIMESTAMP_5_03, TIMESTAMP_5_02, 0 },
+      true
+    },
+    { TIMESTAMP_5_05,
+      TIMESTAMP_5_00,
+      3,
+      false,
+      { TIMESTAMP_5_05, TIMESTAMP_5_03, TIMESTAMP_5_02, 0 },
+      false
+    },
+    { TIMESTAMP_5_04,
+      TIMESTAMP_5_01,
+      3,
+      true,
+      { TIMESTAMP_5_05, TIMESTAMP_5_03, TIMESTAMP_5_02, 0 },
+      true
+    },
+    { TIMESTAMP_5_04,
+      TIMESTAMP_5_01,
+      3,
+      false,
+      { TIMESTAMP_5_03, TIMESTAMP_5_02, 0 },
+      false
+    },
+    { TIMESTAMP_4_59,
+      TIMESTAMP_5_05,
+      3,
+      true,
+      { TIMESTAMP_FIRST, TIMESTAMP_5_00, TIMESTAMP_5_02, 0 },
+      true
+    },
+    { TIMESTAMP_4_59,
+      TIMESTAMP_5_05,
+      3,
+      false,
+      { TIMESTAMP_5_00, TIMESTAMP_5_02, TIMESTAMP_5_03, 0 },
+      false
+    },
+    { TIMESTAMP_5_01,
+      TIMESTAMP_5_07,
+      3,
+      true,
+      { TIMESTAMP_5_00, TIMESTAMP_5_02, TIMESTAMP_5_03, 0 },
+      true
+    },
+    { TIMESTAMP_5_01,
+      TIMESTAMP_5_07,
+      3,
+      false,
+      { TIMESTAMP_5_02, TIMESTAMP_5_03, TIMESTAMP_5_05, 0 },
+      true
+    },
+    { TIMESTAMP_5_00,
+      TIMESTAMP_UNSPECIFIED,
+      3,
+      true,
+      { TIMESTAMP_5_00, TIMESTAMP_5_02, TIMESTAMP_5_03, 0 },
+      true
+    },
+    { TIMESTAMP_5_00,
+      TIMESTAMP_UNSPECIFIED,
+      3,
+      false,
+      { TIMESTAMP_5_00, TIMESTAMP_5_02, TIMESTAMP_5_03, 0 },
+      true
+    },
+    { TIMESTAMP_5_00,
+      TIMESTAMP_UNSPECIFIED,
+      6,
+      true,
+      { TIMESTAMP_5_00, TIMESTAMP_5_02, TIMESTAMP_5_03, TIMESTAMP_5_05, TIMESTAMP_5_06, TIMESTAMP_LAST, 0 },
+      false
+    },
+    { TIMESTAMP_5_00,
+      TIMESTAMP_UNSPECIFIED,
+      6,
+      false,
+      { TIMESTAMP_5_00, TIMESTAMP_5_02, TIMESTAMP_5_03, TIMESTAMP_5_05, TIMESTAMP_5_06, 0 },
+      false
+    },
+    { TIMESTAMP_5_07,
+      TIMESTAMP_UNSPECIFIED,
+      6,
+      true,
+      { TIMESTAMP_5_06, TIMESTAMP_LAST, 0 },
+      false
+    },
+    { TIMESTAMP_5_07,
+      TIMESTAMP_UNSPECIFIED,
+      6,
+      false,
+      { NODATA, 0 },
+      false
+    },
+    { TIMESTAMP_UNSPECIFIED,
+      TIMESTAMP_5_06,
+      3,
+      true,
+      { TIMESTAMP_5_06,TIMESTAMP_5_05,TIMESTAMP_5_03, 0 },
+      true
+    },
+    { TIMESTAMP_UNSPECIFIED,
+      TIMESTAMP_5_06,
+      3,
+      false,
+      { TIMESTAMP_5_06,TIMESTAMP_5_05,TIMESTAMP_5_03, 0 },
+      true
+    },
+    { TIMESTAMP_UNSPECIFIED,
+      TIMESTAMP_5_06,
+      6,
+      true,
+      { TIMESTAMP_5_06,TIMESTAMP_5_05,TIMESTAMP_5_03,TIMESTAMP_5_02,TIMESTAMP_5_00,TIMESTAMP_FIRST, 0 },
+      false
+    },
+    { TIMESTAMP_UNSPECIFIED,
+      TIMESTAMP_5_06,
+      6,
+      false,
+      { TIMESTAMP_5_06, TIMESTAMP_5_05, TIMESTAMP_5_03, TIMESTAMP_5_02, TIMESTAMP_5_00, 0 },
+      false
+    },
+    { TIMESTAMP_UNSPECIFIED,
+      TIMESTAMP_4_48,
+      6,
+      true,
+      { TIMESTAMP_5_00, TIMESTAMP_FIRST, 0 },
+      false
+    },
+    { TIMESTAMP_UNSPECIFIED,
+      TIMESTAMP_4_48,
+      6,
+      false,
+      { NODATA, 0 },
+      false
+    },
+    { TIMESTAMP_4_48,
+      TIMESTAMP_4_48,
+      0,
+      true,
+      { TIMESTAMP_FIRST, TIMESTAMP_5_00, 0 },
+      false
+    },
+    { TIMESTAMP_4_48,
+      TIMESTAMP_4_48,
+      0,
+      false,
+      { NODATA, 0 },
+      false
+    },
+    { TIMESTAMP_4_48,
+      TIMESTAMP_4_48,
+      1,
+      true,
+      { TIMESTAMP_FIRST, 0 },
+      true
+    },
+    { TIMESTAMP_4_48,
+      TIMESTAMP_4_48,
+      1,
+      false,
+      { NODATA, 0 },
+      false
+    },
+    { TIMESTAMP_4_48,
+      TIMESTAMP_4_48,
+      2,
+      true,
+      { TIMESTAMP_FIRST,TIMESTAMP_5_00, 0 },
+      false
+    },
+    { TIMESTAMP_5_00,
+      TIMESTAMP_5_00,
+      0,
+      true,
+      { TIMESTAMP_5_00,TIMESTAMP_5_02, 0 },
+      false
+    },
+    { TIMESTAMP_5_00,
+      TIMESTAMP_5_00,
+      0,
+      false,
+      { TIMESTAMP_5_00, 0 },
+      false
+    },
+    { TIMESTAMP_5_00,
+      TIMESTAMP_5_00,
+      1,
+      true,
+      { TIMESTAMP_5_00, 0 },
+      true
+    },
+    { TIMESTAMP_5_00,
+      TIMESTAMP_5_00,
+      1,
+      false,
+      { TIMESTAMP_5_00, 0 },
+      false
+    },
+    { TIMESTAMP_5_01,
+      TIMESTAMP_5_01,
+      0,
+      true,
+      { TIMESTAMP_5_00, TIMESTAMP_5_02, 0 },
+      false
+    },
+    { TIMESTAMP_5_01,
+      TIMESTAMP_5_01,
+      0,
+      false,
+      { NODATA, 0 },
+      false
+    },
+    { TIMESTAMP_5_01,
+      TIMESTAMP_5_01,
+      1,
+      true,
+      { TIMESTAMP_5_00, 0 },
+      true
+    },
+    { TIMESTAMP_5_01,
+      TIMESTAMP_5_01,
+      1,
+      false,
+      { NODATA },
+      false
+    },
+    {0,0,0,false,{ NODATA }, false} // last element
+};
+#endif /*UA_HISTORICAL_READ_TEST_DATA_H_*/