open62541开发:添加sqlite3 历史数据库

news2024/11/24 14:49:51

            

    历史数据库在OPCUA  应用中十分重要,例如OPCUA 网关和OPCUA 汇聚服务器中都需要历史数据库功能。但是open62541 协议栈中仅包含了基于内存的历史数据库,在实际应用中是不够的。本博文讨论open62541 中添加sqlite3 为基础的历史数据库若干问题。

借鉴

        Github上有一些open62541 添加sqlite 的项目的项目,在CSDN 上

学习open62541 --- [58] 使用数据库存储历史数据

的博文中,介绍了如何将sqlite 改成Linux 下的实现。它的项目在:

GitHub - nicolasr75/open62541_sqlite:  

      他的项目对我帮助很大,但是,它的sqlite 不是加载到open62541 的Plugin 中,而是单独地添加在应用程序的项目中的。 他将c程序和h 文件合在了一起,只有使用sqlite 历史数据时才会被include 并且编译。另一方面,该项目只支持单变量历史数据存储。

      我的项目是将open62541 安装到系统目录中的,所以希望将sqlite 结合到open61541 程序中。另外扩展为多变量历史数据存储。下面记录我的实现过程。

在open62541 包中加入sqlite3 

安装sqlite3 库

sudo apt -y install libsqlite3-dev
 

        将SQLiteBackend.h 放置到open62541 的Plugins/include /plugin/history_data 中。

重新编译open62541 就可以了。也可以单独地将SQLiteBackend.h放置在应用程序中。

修改成多节点存储

        在参考文章中只是单变量存储的方法。不支持多个变量的存储。我将它进行了修改:

方法1:使用NodeId区分不同变量

将MearsuringPointID 改为NodeId.节点的字符串(ns=1;i=5055)。注意要带单括号(‘)。

 strncat(query, "') AND (MeasuringPointID='", QUERY_BUFFER_SIZE);
     char measuringPointID[60];
    snprintf(measuringPointID, 60, "ns=%d;i=%d",nodeId->namespaceIndex,nodeId->identifier.numeric);
    strncat(query, measuringPointID, QUERY_BUFFER_SIZE);

方法2 将不同变量的数据构建不同的表(table)

这种方法的好处是访问数据库效率高。

在这里,我们仅实现第一种方式。

源代码(SQLiteBackend.h)

#ifndef BACKEND_H
#define BACKEND_H

#include <time.h>
#include <stdio.h>
#include <open62541/plugin/historydata/history_data_backend.h>
#include "sqlite3.h"


static const size_t END_OF_DATA = SIZE_MAX;
static const size_t QUERY_BUFFER_SIZE = 500;

char *UA_String2string(UA_String uaString)
{
  char *convert = (char *)UA_malloc(sizeof(char) * uaString.length + 1);
  memcpy(convert, uaString.data, uaString.length);
  convert[uaString.length] = '\0';
  return convert;
}
UA_Int64 convertTimestampStringToUnixSeconds(const char* timestampString)
{
    UA_DateTimeStruct dts;
    memset(&dts, 0, sizeof(dts));
 //   printf("convertTimestampStringToUnixSeconds:%s\n",timestampString);
    sscanf(timestampString, "%hu-%hu-%hu %hu:%hu:%hu", 
        &dts.year, &dts.month, &dts.day, &dts.hour, &dts.min, &dts.sec);
   // printf("%d %d\n",dts.year, dts.month);
    UA_DateTime dt = UA_DateTime_fromStruct(dts);

    UA_Int64 t = UA_DateTime_toUnixTime(dt);
 //   printf("convertTimestampStringToUnixSeconds=%lu\n",t);
    return t;
}


const char* convertUnixSecondsToTimestampString(UA_Int64 unixSeconds)
{
    static char buffer[20];

   //  printf("convertUnixSecondsToTimestampString=%lu\n",unixSeconds);
    UA_DateTime dt = UA_DateTime_fromUnixTime(unixSeconds);
    UA_DateTimeStruct dts = UA_DateTime_toStruct(dt);

    struct tm tm;
    memset(&tm, 0, sizeof(tm));
    tm.tm_year = dts.year- 1900;
    tm.tm_mon  = dts.month - 1;
    tm.tm_mday = dts.day;
    tm.tm_hour = dts.hour;
    tm.tm_min  = dts.min;
    tm.tm_sec  = dts.sec;
   // printf("=%d %d\n",tm.tm_year,tm.tm_mon);
    memset(buffer, 0, 20);
    
    strftime(buffer, 20, "%Y-%m-%d %H:%M:%S", &tm);
  //  printf("convertUnixSecondsToTimestampString:%s\n",buffer);
    return buffer;
}



//Context that is needed for the SQLite callback for copying data.
struct context_copyDataValues {
    size_t maxValues;
    size_t counter;
    UA_DataValue *values;
};

typedef  struct context_copyDataValues  context_copyDataValues;


struct context_sqlite {

    sqlite3* sqlite;

    const char* measuringPointID;
};


static struct context_sqlite*
generateContext_sqlite(const char* filename) 
{

    sqlite3* handle;
    char *errorMessage;

    int res = sqlite3_open(filename, &handle);

    if (res != SQLITE_OK)
        return NULL;

    struct context_sqlite* ret = (struct context_sqlite*)UA_calloc(1, sizeof(struct context_sqlite));
    if (ret == NULL)
    {
        return NULL;
    }

    const char *sql = "DROP TABLE IF EXISTS PeriodicValues;" 
                      "CREATE TABLE PeriodicValues(MeasuringPointID STRING, Value DOUBLE, Timestamp DATETIME DEFAULT CURRENT_TIMESTAMP);";

    
    res = sqlite3_exec(handle, sql, NULL, NULL, &errorMessage);
    if (res != SQLITE_OK)
    {
        printf("%s | Error | %s\n", __func__, errorMessage);
        sqlite3_free(errorMessage);
        sqlite3_close(handle);

        return NULL;
    }


    ret->sqlite = handle;

    //For this demo we have only one source measuring point which we hardcode in the context.
    //A more advanced demo should determine the available measuring points from the source
    //itself or maybe an external configuration file.
    ret->measuringPointID = "1";

    return ret;
}


static UA_StatusCode
serverSetHistoryData_sqliteHDB(UA_Server *server,
	void *hdbContext,
	const UA_NodeId *sessionId,
	void *sessionContext,
	const UA_NodeId *nodeId,
	UA_Boolean historizing,
	const UA_DataValue *value)
{
    struct context_sqlite* context = (struct context_sqlite*)hdbContext;

    size_t result;
    char* errorMessage;

    char query[QUERY_BUFFER_SIZE];
  //  UA_String Id=nodeId->identifier.string;
    char measuringPointID[60];
    snprintf(measuringPointID, 60, "ns=%d;i=%d",nodeId->namespaceIndex,nodeId->identifier.numeric);
    strncpy(query, "INSERT INTO PeriodicValues VALUES('", QUERY_BUFFER_SIZE);
     strncat(query, measuringPointID, QUERY_BUFFER_SIZE);
    if (value->hasValue && 
        value->status == UA_STATUSCODE_GOOD &&
        value->value.type == &UA_TYPES[UA_TYPES_DOUBLE])
    {
        char remaining[60];
        snprintf(remaining, 60, "',%f, CURRENT_TIMESTAMP);", *(double*)(value->value.data));//datetime(CURRENT_TIMESTAMP,'localtime')
        strncat(query, remaining, QUERY_BUFFER_SIZE);
    }
    else
    {
        printf("%s | Error | historical value is invalid\n", __func__);
        return UA_STATUSCODE_BADINTERNALERROR;
    }
    printf("serverSetHistoryData_sqliteHDB:%s\n",query);
    int res = sqlite3_exec(context->sqlite, query, NULL, NULL, &errorMessage);
    if (res != SQLITE_OK)
    {
        printf("%s | Error | %s\n", __func__, errorMessage);
        sqlite3_free(errorMessage);

        return UA_STATUSCODE_BADINTERNALERROR;
    }

    return UA_STATUSCODE_GOOD;
}


static size_t
getEnd_sqliteHDB(UA_Server *server,
	void *hdbContext,
	const UA_NodeId *sessionId,
	void *sessionContext,
	const UA_NodeId *nodeId)
{
    return END_OF_DATA;
}


//This is a callback for all queries that return a single timestamp as the number of Unix seconds
static int timestamp_callback(void* result, int count, char **data, char **columns)
{
    *(UA_Int64*)result = convertTimestampStringToUnixSeconds(data[0]);
    printf("timestamp_callback:%s\n",data[0]);
 //   printf("timestamp_callback%lu\n",*(UA_Int64*)result);
    return 0;
}


static int resultSize_callback(void* result, int count, char **data, char **columns)
{
    *(size_t*)result = strtol(data[0], NULL, 10);

    return 0;
}


static size_t
lastIndex_sqliteHDB(UA_Server *server,
    void *hdbContext,
    const UA_NodeId *sessionId,
    void *sessionContext,
    const UA_NodeId *nodeId)
{
    struct context_sqlite* context = (struct context_sqlite*)hdbContext;

    size_t result;
    char* errorMessage;

    char query[QUERY_BUFFER_SIZE];
    strncpy(query, "SELECT Timestamp FROM PeriodicValues WHERE MeasuringPointID='", QUERY_BUFFER_SIZE);
     char measuringPointID[60];
    snprintf(measuringPointID, 60, "ns=%d;i=%d",nodeId->namespaceIndex,nodeId->identifier.numeric);
    strncat(query, measuringPointID, QUERY_BUFFER_SIZE);
    strncat(query, "'  ORDER BY Timestamp DESC LIMIT 1", QUERY_BUFFER_SIZE);//DESC
    printf("lastIndex:%s\n",query);
    int res = sqlite3_exec(context->sqlite, query, timestamp_callback, &result, &errorMessage);

    if (res != SQLITE_OK)
    {
        printf("%s | Error | %s\n", __func__, errorMessage);
        sqlite3_free(errorMessage);
        return END_OF_DATA;
    }

    return result;
}

static size_t
firstIndex_sqliteHDB(UA_Server *server,
    void *hdbContext,
    const UA_NodeId *sessionId,
    void *sessionContext,
    const UA_NodeId *nodeId)
{
    struct context_sqlite* context = (struct context_sqlite*)hdbContext;

    size_t result;
    char* errorMessage;

    char query[QUERY_BUFFER_SIZE];
    strncpy(query, "SELECT Timestamp FROM PeriodicValues WHERE MeasuringPointID='", QUERY_BUFFER_SIZE);
     char measuringPointID[60];
    snprintf(measuringPointID, 60, "ns=%d;i=%d",nodeId->namespaceIndex,nodeId->identifier.numeric);
    strncat(query, measuringPointID, QUERY_BUFFER_SIZE);
    strncat(query, "' ORDER BY Timestamp LIMIT 1", QUERY_BUFFER_SIZE);

    int res = sqlite3_exec(context->sqlite, query, timestamp_callback, &result, &errorMessage);
    printf("firsrIndex:%s\n",query);
    if (res != SQLITE_OK)
    {
        printf("%s | Error | %s\n", __func__, errorMessage);
        sqlite3_free(errorMessage);
        return END_OF_DATA;
    }

    return result;
}


static UA_Boolean
search_sqlite(struct context_sqlite* context,const UA_NodeId *nodeId,
	UA_Int64 unixSeconds, MatchStrategy strategy,
	size_t *index) 
{	
    *index = END_OF_DATA; // TODO
    char* errorMessage;

    char query[QUERY_BUFFER_SIZE];
    strncpy(query, "SELECT Timestamp FROM PeriodicValues WHERE MeasuringPointID='", QUERY_BUFFER_SIZE);
     char measuringPointID[60];
    snprintf(measuringPointID, 60, "ns=%d;i=%d",nodeId->namespaceIndex,nodeId->identifier.numeric);
    strncat(query, measuringPointID, QUERY_BUFFER_SIZE);
    strncat(query, "' AND ", QUERY_BUFFER_SIZE);

    switch (strategy)
    {
    case MATCH_EQUAL_OR_AFTER:
        strncat(query, "Timestamp>='", QUERY_BUFFER_SIZE);
        strncat(query, convertUnixSecondsToTimestampString(unixSeconds), QUERY_BUFFER_SIZE);
        strncat(query, "' ORDER BY Timestamp LIMIT 1", QUERY_BUFFER_SIZE);
        break;
    case MATCH_AFTER:
        strncat(query, "Timestamp>'", QUERY_BUFFER_SIZE);
        strncat(query, convertUnixSecondsToTimestampString(unixSeconds), QUERY_BUFFER_SIZE);
        strncat(query, "' ORDER BY Timestamp LIMIT 1", QUERY_BUFFER_SIZE);
        break;
    case MATCH_EQUAL_OR_BEFORE:
        strncat(query, "Timestamp<='", QUERY_BUFFER_SIZE);
        strncat(query, convertUnixSecondsToTimestampString(unixSeconds), QUERY_BUFFER_SIZE);
        strncat(query, "' ORDER BY Timestamp DESC LIMIT 1", QUERY_BUFFER_SIZE);
        break;
    case MATCH_BEFORE:
        strncat(query, "Timestamp<'", QUERY_BUFFER_SIZE);
        strncat(query, convertUnixSecondsToTimestampString(unixSeconds), QUERY_BUFFER_SIZE);
        strncat(query, "' ORDER BY Timestamp DESC LIMIT 1", QUERY_BUFFER_SIZE);
        break;
    default:
        return false;
    }

    printf("search_sqlite:%s\n",query);
    int res = sqlite3_exec(context->sqlite, query, timestamp_callback, index, &errorMessage);

    if (res != SQLITE_OK)
    {
        printf("%s | Error | %s\n", __func__, errorMessage);
        sqlite3_free(errorMessage);
        return false;
    }
    else
    {
        return true;
    }

}

static size_t
getDateTimeMatch_sqliteHDB(UA_Server *server,
    void *hdbContext,
    const UA_NodeId *sessionId,
    void *sessionContext,
    const UA_NodeId *nodeId,
    const UA_DateTime timestamp,
    const MatchStrategy strategy)
{
    struct context_sqlite* context = (struct context_sqlite*)hdbContext;

    UA_Int64 ts =UA_DateTime_toUnixTime(timestamp);
    printf("getDateTimeMatch_sqliteHDB:%s\n",convertUnixSecondsToTimestampString(ts));
    printf("strategy:%u\n",strategy);
    size_t result = END_OF_DATA;

    UA_Boolean res = search_sqlite(context,nodeId, ts, strategy, &result);

    return result;
}


static size_t
resultSize_sqliteHDB(UA_Server *server,
    void *hdbContext,
    const UA_NodeId *sessionId,
    void *sessionContext,
    const UA_NodeId *nodeId,
    size_t startIndex,
    size_t endIndex)
{
    struct context_sqlite* context = (struct context_sqlite*)hdbContext;

    char* errorMessage;
    size_t result = 0;
    printf("resultSize_sqliteHDB:startIndex:%lu\n",startIndex);
     printf("resultSize_sqliteHDB:endIndex:%lu\n",endIndex);
    char query[QUERY_BUFFER_SIZE];
    strncpy(query, "SELECT COUNT(*) FROM PeriodicValues WHERE ", QUERY_BUFFER_SIZE);
    strncat(query, "(Timestamp>='", QUERY_BUFFER_SIZE);
    strncat(query, convertUnixSecondsToTimestampString(startIndex), QUERY_BUFFER_SIZE);
    strncat(query, "') AND (Timestamp<='", QUERY_BUFFER_SIZE);
    strncat(query, convertUnixSecondsToTimestampString(endIndex), QUERY_BUFFER_SIZE);
    strncat(query, "') AND (MeasuringPointID='", QUERY_BUFFER_SIZE);
     char measuringPointID[60];
    snprintf(measuringPointID, 60, "ns=%d;i=%d",nodeId->namespaceIndex,nodeId->identifier.numeric);
    strncat(query, measuringPointID, QUERY_BUFFER_SIZE);
    strncat(query, "')", QUERY_BUFFER_SIZE);
    printf("resultSize_sqliteHDB:%s\n",query);
    int res = sqlite3_exec(context->sqlite, query, resultSize_callback, &result, &errorMessage);

    if (res != SQLITE_OK)
    {
        printf("%s | Error | %s\n", __func__, errorMessage);
        sqlite3_free(errorMessage);
        return 0; // no data
    }

    return result;
}


static int copyDataValues_callback(void* result, int count, char **data, char **columns)
{
    UA_DataValue dv;
    UA_DataValue_init(&dv);

    dv.status = UA_STATUSCODE_GOOD;
    dv.hasStatus = true;

    dv.sourceTimestamp = UA_DateTime_fromUnixTime(convertTimestampStringToUnixSeconds(data[0]));
    dv.hasSourceTimestamp = true;

    dv.serverTimestamp = dv.sourceTimestamp;
    dv.hasServerTimestamp = true;

    double value = strtod(data[1], NULL);

    UA_Variant_setScalarCopy(&dv.value, &value, &UA_TYPES[UA_TYPES_DOUBLE]);
    dv.hasValue = true;

    context_copyDataValues* ctx = (context_copyDataValues*)result;

    UA_DataValue_copy(&dv, &ctx->values[ctx->counter]);

    ctx->counter++;

    if (ctx->counter == ctx->maxValues)
    {
        return 1;
    }
    else
    {
        return 0;
    }
}


static UA_StatusCode
copyDataValues_sqliteHDB(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 maxValues,
    UA_NumericRange range,
    UA_Boolean releaseContinuationPoints,
    const UA_ByteString *continuationPoint,
    UA_ByteString *outContinuationPoint,
    size_t *providedValues,
    UA_DataValue *values)
{
    //NOTE: this demo does not support continuation points!!!
    struct context_sqlite* context = (struct context_sqlite*)hdbContext;

    char* errorMessage;
  //  const char* measuringPointID = "1";

    char query[QUERY_BUFFER_SIZE];
    strncpy(query, "SELECT Timestamp, Value FROM PeriodicValues WHERE ", QUERY_BUFFER_SIZE);
    strncat(query, "(Timestamp>='", QUERY_BUFFER_SIZE);
    strncat(query, convertUnixSecondsToTimestampString(startIndex), QUERY_BUFFER_SIZE);
    strncat(query, "') AND (Timestamp<='", QUERY_BUFFER_SIZE);
    strncat(query, convertUnixSecondsToTimestampString(endIndex), QUERY_BUFFER_SIZE);
    strncat(query, "') AND (MeasuringPointID='", QUERY_BUFFER_SIZE);
     char measuringPointID[60];
    snprintf(measuringPointID, 60, "ns=%d;i=%d",nodeId->namespaceIndex,nodeId->identifier.numeric);
    strncat(query, measuringPointID, QUERY_BUFFER_SIZE);
    strncat(query, "')", QUERY_BUFFER_SIZE);

    context_copyDataValues ctx;
    ctx.maxValues = maxValues;
    ctx.counter = 0;
    ctx.values = values;

    int res = sqlite3_exec(context->sqlite, query, copyDataValues_callback, &ctx, &errorMessage);

    if (res != SQLITE_OK)
    {
        if (res == SQLITE_ABORT) // if reach maxValues, then request abort, so this is not error
        {
            sqlite3_free(errorMessage);
            return UA_STATUSCODE_GOOD;
        }
        else
        {
            printf("%s | Error | %s\n", __func__, errorMessage);
            sqlite3_free(errorMessage);
            return UA_STATUSCODE_BADINTERNALERROR;
        }
            
    }
    else
    {
        return UA_STATUSCODE_GOOD;
    }
}

static const UA_DataValue*
getDataValue_sqliteHDB(UA_Server *server,
    void *hdbContext,
    const UA_NodeId *sessionId,
    void *sessionContext,
    const UA_NodeId *nodeId,
	size_t index)
{
    struct context_sqlite* context = (struct context_sqlite*)hdbContext;

    return NULL;
}


static UA_Boolean
boundSupported_sqliteHDB(UA_Server *server,
    void *hdbContext,
    const UA_NodeId *sessionId,
    void *sessionContext,
    const UA_NodeId *nodeId)
{
	return false; // We don't support returning bounds in this demo
}


static UA_Boolean
timestampsToReturnSupported_sqliteHDB(UA_Server *server,
    void *hdbContext,
    const UA_NodeId *sessionId,
    void *sessionContext,
    const UA_NodeId *nodeId,
    const UA_TimestampsToReturn timestampsToReturn)
{
    return true;
}


UA_HistoryDataBackend
UA_HistoryDataBackend_sqlite(const char* filename)
{
    UA_HistoryDataBackend result;
    memset(&result, 0, sizeof(UA_HistoryDataBackend));
    result.serverSetHistoryData = &serverSetHistoryData_sqliteHDB;
    result.resultSize = &resultSize_sqliteHDB;
    result.getEnd = &getEnd_sqliteHDB;
    result.lastIndex = &lastIndex_sqliteHDB;
    result.firstIndex = &firstIndex_sqliteHDB;
    result.getDateTimeMatch = &getDateTimeMatch_sqliteHDB;
    result.copyDataValues = &copyDataValues_sqliteHDB;
    result.getDataValue = &getDataValue_sqliteHDB;
    result.boundSupported = &boundSupported_sqliteHDB;
    result.timestampsToReturnSupported = &timestampsToReturnSupported_sqliteHDB;
    result.deleteMembers = NULL; // We don't support deleting in this demo
    result.getHistoryData = NULL; // We don't support the high level API in this demo
    result.context = generateContext_sqlite(filename);
    return result;
}


#endif

应用程序测试

server 修改为添加两个变量。NodeId 使用Numeric 方式。

源代码(server.h)

#include <signal.h>
#include <open62541/server.h>
#include <open62541/server_config_default.h>
#include <open62541/plugin/log_stdout.h>
#include <open62541/plugin/historydata/history_data_gathering_default.h>
#include <open62541/plugin/historydata/history_database_default.h>
#include <open62541/plugin/historydatabase.h>
#include "SQLiteBackend.h"


static UA_Boolean running = true;

static void stopHandler(int sign) 
{
    (void)sign;
    UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "received ctrl-c");
    running = false;
}

UA_NodeId addHistoryVariable(UA_Server *server,char *VairableName){
     /* Define the attribute of the uint32 variable node */
    UA_VariableAttributes attr = UA_VariableAttributes_default;
    UA_Double myDouble = 17.2;
    UA_Variant_setScalar(&attr.value, &myDouble, &UA_TYPES[UA_TYPES_DOUBLE]);
    attr.description = UA_LOCALIZEDTEXT("en-US", VairableName);
    attr.displayName = UA_LOCALIZEDTEXT("en-US", VairableName);
    attr.dataType = UA_TYPES[UA_TYPES_DOUBLE].typeId;

    /*
    * We set the access level to also support history read
    * This is what will be reported to clients
    */
    attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE | UA_ACCESSLEVELMASK_HISTORYREAD;

    /*
    * We also set this node to historizing, so the server internals also know from it.
    */
    attr.historizing = true;

    /* Add the variable node to the information model */
    UA_NodeId doubleNodeId = UA_NODEID_STRING(1, VairableName);
    UA_QualifiedName doubleName = UA_QUALIFIEDNAME(1, VairableName);
    UA_NodeId parentNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER);
    UA_NodeId parentReferenceNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES);
    UA_NodeId outNodeId;
    UA_NodeId_init(&outNodeId);
    UA_StatusCode retval = UA_Server_addVariableNode(server,
        UA_NODEID_NULL,
        parentNodeId,
        parentReferenceNodeId,
        doubleName,
        UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE),
        attr,
        NULL,
        &outNodeId);

    UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "UA_Server_addVariableNode %s", UA_StatusCode_name(retval));

    
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER,"addHistoryVariable:%d",outNodeId.identifier.numeric);
return outNodeId;
}
int main(void) 
{
    signal(SIGINT, stopHandler);
    signal(SIGTERM, stopHandler);


    UA_Server *server = UA_Server_new();
    UA_ServerConfig *config = UA_Server_getConfig(server);
    UA_ServerConfig_setDefault(config);

 
    UA_HistoryDataGathering gathering = UA_HistoryDataGathering_Default(1);

   
    config->historyDatabase = UA_HistoryDatabase_default(gathering);

UA_NodeId VariableANodeId=addHistoryVariable(server,"myDoubleValueA");
UA_NodeId VariableBNodeId=addHistoryVariable(server,"myDoubleValueB");
   
    UA_HistorizingNodeIdSettings setting;


    setting.historizingBackend = UA_HistoryDataBackend_sqlite("database.sqlite");

    
    setting.maxHistoryDataResponseSize = 100;


    setting.historizingUpdateStrategy = UA_HISTORIZINGUPDATESTRATEGY_VALUESET;

    
    UA_StatusCode   retval = gathering.registerNodeId(server, gathering.context, &VariableANodeId, setting);
  retval = gathering.registerNodeId(server, gathering.context, &VariableBNodeId, setting);
    UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "registerNodeId %s", UA_StatusCode_name(retval));
 
 retval = UA_Server_run(server, &running);
    UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "UA_Server_run %s", UA_StatusCode_name(retval));


    UA_Server_delete(server);

    return (int)retval;

}

使用DB Browser for SQLite 工具查看数据库数据

下载一个DB Browser for SQLite 工具可以查看数据库中的数据:

使用uaExpert

         使用uaExpert 修改变量的数据 ,并且查看历史数据。具体方法可以参照引用的博文。特别注意的是;在数据库中存储的时间标签是UTC标准时间,而uaExpert 使用的是北京时间,它们相差了8小时。在查看历史数据时,要注意起始时间和结束时间,设置错了,不会读出。可以通过DB Browser 查看记录数据的时间标签,并将小时加8。

结束语

        开源软件并不完全是拿来就够了。真正实际应用,需要大量的开发工作。要充分估计开源软件的学习和扩展功能的工作量。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1041853.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Python小练习】简单浮点矩阵乘法

前言 最近上《计算机控制系统》课&#xff0c;涉及许多矩阵运算&#xff08;乘法居多&#xff09;&#xff0c;觉得手算不过来&#xff0c;按计算器太慢&#xff0c;于是写一个Python小程序做做。 二、代码 import numpy as np from numpy import shapem int(input("…

3D点云数据集制作实录【LiDAR】

在过去的两年里&#xff0c;我一直在和机器人打交道。 今年早些时候&#xff0c;我不再只关注相机&#xff0c;而是决定开始使用激光雷达。 因此&#xff0c;经过大量研究后&#xff0c;我选择了 32 束 RoboSense 设备。 推荐&#xff1a;用 NSDT编辑器 快速搭建可编程3D场景 …

ElasticSearch深度分页解决方案

文章目录 概要ElasticSearch介绍es分页方法es分页性能对比表方案对比 From/Size参数深度分页问题Scroll#性能对比向前翻页 总结个人思考 概要 好久没更新文章了&#xff0c;最近研究了一下es的深分页解决方案。和大家分享一下&#xff0c;祝大家国庆节快乐。 ElasticSearch介…

记一次 .NET 某拍摄监控软件 卡死分析

一&#xff1a;背景 1. 讲故事 今天本来想写一篇 非托管泄露 的生产事故分析&#xff0c;但想着昨天就上了一篇非托管文章&#xff0c;连着写也没什么意思&#xff0c;换个口味吧&#xff0c;刚好前些天有位朋友也找到我&#xff0c;说他们的拍摄监控软件卡死了&#xff0c;让…

Android Shape设置背景

设置背景时&#xff0c;经常这样 android:background“drawable/xxx” 。如果是纯色图片&#xff0c;可以考虑用 shape 替代。 shape 相比图片&#xff0c;减少资源占用&#xff0c;缩减APK体积。 开始使用。 <?xml version"1.0" encoding"utf-8"?…

【STM32单片机】u8g2智能风扇设计

文章目录 一、功能简介二、软件设计三、实验现象联系作者 一、功能简介 本项目使用STM32F103C8T6单片机控制器&#xff0c;使用按键、IIC OLED模块、DS18B20温度传感器、直流电机、红外遥控等。 主要功能&#xff1a; 初始化后进入温度显示界面&#xff0c;系统初始状态为手动…

探索视听新纪元: ChatGPT的最新语音和图像功能全解析

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f405;&#x1f43e;猫头虎建议程序员必备技术栈一览表&#x1f4d6;&#xff1a; &#x1f916; 人工智能 AI: &#x1f9e0; Machine …

【我的创作纪念日】使用pix2pixgan实现barts2020数据集的处理(完整版本)

使用pix2pixgan &#xff08;pytorch)实现T1 -> T2的基本代码 使用 https://github.com/eriklindernoren/PyTorch-GAN/ 这里面的pix2pixgan代码进行实现。 进去之后我们需要重新处理数据集&#xff0c;并且源代码里面先训练的生成器&#xff0c;后训练鉴别器。 一般情况下…

亚马逊要求的UL报告的产品标准是什么?如何区分

亚马逊为什么要求电子产品有UL检测报告&#xff1f; 首先&#xff0c;美国是一个对安全要求非常严格的国家&#xff0c;美国本土的所有电子产品生产企业早在很多年前就要求有相关安规检测。 其次&#xff0c;随着亚马逊在全球商业的战略地位不断提高&#xff0c;境外的电子设…

百度资源搜索平台出现:You do not have the proper credential to access this page.怎么办?

Forbidden site not allowed You do not have the proper credential to access this page. If you think this is a server error, please contact the webmaster. 如果你的百度资源平台&#xff0c;点进去出现这个提示&#xff0c;说明您的网站已经被百度清退了。 如果你的网…

队列的分类及用途

队列&#xff08;Queue&#xff09;是一种常见的数据结构&#xff0c;用于存储和管理数据元素。队列通常遵循先进先出&#xff08;FIFO&#xff0c;First-In-First-Out&#xff09;的原则&#xff0c;这意味着最早添加到队列的元素将首先被移除。队列有不同的类型和用途&#x…

VS code本地安装PlantUML

VS code本地安装PlantUML 需要条件vs code安装插件使用常见错误 需要条件 在VS Code上安装PlantUML扩展之前&#xff0c;请确保您具有以下先决条件: : Java与GraphViz(点击可直接跳转下载界面); 安装省略 vs code安装插件 vs code安装以下两个插件&#xff08;PlantUML,Grap…

易云维®智慧工厂数字化管理平台助推工业制造企业数字化转型新动能

近年来&#xff0c;我国正在积极推进工业制造企业数字化转型&#xff0c;工业制造企业数字化转型迎来了密集的利好政策&#xff0c;近期&#xff0c;国家工信部又出台系列政策&#xff0c;实施工业制造企业数字化促进工程&#xff0c;推动工业制造企业更快更好地拥抱数字经济。…

数字安全设备制造有哪几种方式?

数字安全设备制造是指制造用于保护数字信息系统和网络安全的专用设备。以下是几种常见的数字安全设备制造方式&#xff1a; 集成式安全设备制造&#xff1a;集成式安全设备制造是将多种安全功能集成到单一的硬件设备或软件平台中。这种制造方式可以大大降低设备的成本和复杂性&…

vue3 + vite3 addRoute 实现权限管理系统

vue3 vite3 addRoute 实现权限控制 1、前言2、静态路由3、动态路由4、在组建中使用路由5、注意事项 1、前言 在权限系统开发中&#xff0c;根据后端返回的菜单列表动态添加路由是非常常见的需求&#xff0c;它可以实现根据用户权限动态加载可访问的页面。本篇文章我们将重点介…

第二届全国高校计算机技能竞赛——Java赛道

第二届全国高校计算机技能竞赛——Java赛道 小赛跳高 签到题 import java.util.*; public class Main{public static void main(String []args) {Scanner sc new Scanner(System.in);double n sc.nextDouble();for(int i 0; i < 4; i) {n n * 0.9;}System.out.printf(&…

探索公共厕所的数字化治理,智慧公厕完善公共厕所智能化的治理体系

随着城市化进程的不断发展&#xff0c;公共厕所治理成为一个不容忽视的问题。如何通过数字化手段来提升公共厕所管理水平&#xff0c;成为了一个备受关注的话题。本文将以智慧公厕领先厂家广州中期科技有限公司&#xff0c;大量精品案例项目实景实图&#xff0c;探讨公共厕所数…

品牌线上假货怎么治理

随着品牌的发展&#xff0c;母婴、家电、百货等行业&#xff0c;链接量暴增&#xff0c;销售店铺也较多&#xff0c;线上仅通过图片销售的形式&#xff0c;也导致了假货链接地滋生&#xff0c;假货分两种情况&#xff0c;一种是只销售假货的店铺&#xff0c;一种是真假混卖的店…

用numpy生成18种特殊数组

文章目录 单值数组特殊矩阵范德蒙德矩阵数值范围坐标网格绘图代码 所有创建数组的函数中&#xff0c;都有一个可选参数dtype&#xff0c;表示创建的数组的数据类型。 指定维度empty, eye, identity, ones, zeros, full模仿维度empty_like, ones_like, zeros_like, full_like特…

【Linux】C语言实现对文件的加密算法

异或加密 解密方式是进行第二次加密后自动解密 #define BUF_SIZE (16384) //16k /************************************************************** 功能描述: 加密实现 输入参数: --------------------------------------------------------------- 修改作者: 修改日期…