当前位置: 首页 > news >正文

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

            

    历史数据库在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);//DESCprintf("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; // TODOchar* 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 demoresult.getHistoryData = NULL; // We don't support the high level API in this demoresult.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。

结束语

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

相关文章:

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

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

美国零售电商平台Target,值得入驻吗?如何入驻?

Target 是美国最大的零售商之一&#xff0c;在品牌出海为大势所趋的背景下&#xff0c;它在北美电商中的地位节节攀升。Target 商店在众多垂直领域提供各种价格实惠的自有品牌&#xff0c;吸引越来越多的跨境商家入驻&#xff0c;如美妆、家居、鞋服、日用百货等&#xff0c;随…...

docker freeswitch mysql驱动相关

1. docker环境 的freeswitch要连mysql数据库 2. centos版本&#xff1a;7.9 3. mysql版本&#xff1a;mysql8.0 4. odbc版本&#xff1a;适用以下命令查询 rpm -qa | grep -E "unixODBC|mysql-connector-odbc" mysql-connector-odbc-5.2.5-8.el7.x86_64 unixODB…...

Chrome iframe 跨域失败

原因&#xff1a; Chrome 稳定版禁用 document.domain setter 解决方案&#xff08;4种&#xff09;&#xff1a; 1 使用低版本Chrome 缺点&#xff1a;无法保证用户浏览器版本。 2 浏览器设置&#xff1a; 浏览器地址栏输入 chrome://flags/#origin-agent-cluster-default然…...

【Vue】vue-cli一站式搭建SPA项目

&#x1f389;&#x1f389;欢迎来到我的CSDN主页&#xff01;&#x1f389;&#x1f389; &#x1f3c5;我是Java方文山&#xff0c;一个在CSDN分享笔记的博主。&#x1f4da;&#x1f4da; &#x1f31f;推荐给大家我的专栏《Vue快速入门》。&#x1f3af;&#x1f3af; &…...

CPP代码检查工具

系列文章目录 提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加 TODO:写完再整理 文章目录 系列文章目录前言CPP代码检查工具工具一:GCC编译器工具二:Cppcheck工具三:gbd调试器工具四:SonarCube嵌入式平台工具内存泄漏检查工具linux的cpu占用情况前言 认…...

在SpringBoot中利用Redis实现互斥锁

在SpringBoot中利用Redis实现互斥锁 基本知识 前提条件&#xff0c;有一个能够在Springboot中使用Redis的项目&#xff0c;或者能够直接开也行 为什么要实现互斥锁&#xff1a;当我们利用Redis存储热点数据时&#xff0c;突然就过期失效或者被删除了&#xff0c;导致大量请求同…...

vue3+eleement plus日历选择季度

<template><div class"el-quarter-wrap"><el-popover width"280" v-model"visible"><template #reference><el-input v-model"quarterDate" placeholder"请选择季度" clearable :prefix-icon&qu…...

实现动态业务规则的方法(Java)

实现动态业务规则的方法&#xff08;Java&#xff09; 企业信息化系统核心在于业务领域的概念模型及于此基础上复杂多变的业务规则&#xff0c;实现中通常抽象规则的接口方法&#xff0c;使用继承或策略等设计模式实现不同的业务规则的实现。领域的概念模型在特定领域是稳定的…...

leetcodeTOP100(26)两数相加

给你两个 非空 的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的&#xff0c;并且每个节点只能存储 一位 数字。 请你将两个数相加&#xff0c;并以相同形式返回一个表示和的链表。 你可以假设除了数字 0 之外&#xff0c;这两个数都不会以 0 …...

performance_schema

插桩名称的最左边部分表示插桩类型&#xff0c;其余部分从左到右依次表示到特定的子系统 mysql> select * from performance_schema.setup_instruments where documentation is not null limit 5,5\G; *************************** 1. row ***************************NAME:…...

全新UI基于Thinkphp的最新自助打印系统/云打印小程序源码/附教程

这是一款全新的基于Thinkphp的最新自助打印系统&#xff0c;最新UI界面设计的云打印小程序源码&#xff0c;带有简单的教程。 下载地址&#xff1a;https://bbs.csdn.net/topics/617324130...

Android 13.0 framework层系统手势增加上滑手势home事件功能(相当于Home键)

1.概述 在13.0的定制化开发系统手势功能的时候,客户需求要求在上滑手势的时候,在底部上滑时候进入系统桌面,也就是增加 home键功能,所以就需要分析相关的系统手势上滑事件,然后添加home事件这样 就可以实现这个功能了 2.framework层系统手势增加上滑手势home事件功能的核…...

webp格式及其转成

"WebP" 是一种现代的图像压缩格式&#xff0c;由谷歌公司开发。它旨在提供高质量的图像压缩&#xff0c;同时减小图像文件的大小&#xff0c;从而加快网络加载速度。WebP 格式通常使用 ".webp" 扩展名来标识。 WebP 图像格式主要有以下几个特点和优点&…...

echo cat find grep命令

目录 cat echo grep find cat cat命令可以理解为英文单词concatenate的缩写&#xff0c;其功能是连接多个文件并且打印到屏幕输出&#xff0c;或者重定向到指定文件中。此命令常用于显示单个文件内容&#xff0c;或者将几个文件内容连接起来一起显示&#xff0c;还可以从标…...

Linux学习第20天:Linux按键输入驱动开发: 大道至简 量入为出

Linux版本号4.1.15 芯片I.MX6ULL 大叔学Linux 品人间百味 思文短情长 中国文化博大精深&#xff0c;太极八卦&#xff0c;阴阳交合&#xff0c;变化无穷。在程序的开发中也是这样&#xff0c;数字0和1也是同样的道理。就本节来说&am…...

WordPress主题开发( 七)之—— 模版文件继承规则

WordPress主题开发&#xff08; 七&#xff09;之—— 模版文件继承规则 概述模板文件层次结构示例可视化概述层次结构详细信息主页显示首页显示单文章页面单页分类目录标签自定义分类自定义文章类型作者显示日期搜索结果404&#xff08;未找到&#xff09;附件嵌入功能非ASCII…...

Simulink 封装

快捷键&#xff1a; Edit Mask&#xff1a;CtrlM Look Under Mask&#xff1a;CtrlU 封装之后的模型&#xff1a; Edit Mask界面&#xff1a; 双击模块后的提示界面&#xff1a; 封装的模块内部&#xff1a;...

【AI视野·今日Robot 机器人论文速览 第三十六期】Tue, 19 Sep 2023

AI视野今日CS.Robotics 机器人学论文速览 Tue, 19 Sep 2023 (showing first 100 of 112 entries) Totally 112 papers &#x1f449;上期速览✈更多精彩请移步主页 Interesting: &#x1f4da;In-Hand Object Rotation, RotateIt 提出了一种基于视觉与触觉的物体旋转朝向的方法…...

Java随笔

动态SQL 是指根据不同的条件或参数生成不同的SQL语句的技术。在实际开发中&#xff0c;我们经常需要根据用户的输入或其他条件来生成不同的SQL语句&#xff0c;动态SQL就能满足这个需求。 在Java中&#xff0c;使用MyBatis作为ORM框架时&#xff0c;可以通过在Mapper.xml文件…...

ARINC825规范简介

ARINC825规范简介 机载CAN网络通用标准 ARINC825规范全称为机载CAN网络通用标准&#xff08;The General Standardization of CAN for Airborne Use&#xff09;。顾名思义&#xff0c;ARINC825规范是建立在CAN物理网络基础上的高层规范。CAN网络使用共享的双绞电缆传输数据&…...

SQLAlchemy列参数的使用和query函数的使用

目录 Column常用参数 代码演示 代码刨析 query函数的使用 基本用法 常见用法示例 查询所有记录 根据条件查询 查询第一条符合条件的记录 查询特定列的值 添加排序规则 使用聚合函数 连接查询 使用filter_by Column常用参数 primary_key&#xff1a;True设置某个字…...

产权未转移登记的离婚析产协议不能对抗债权人

债权人代位析产纠纷作为一个新的民事案由&#xff0c;是民事执行阶段中债务人不能到期清偿债务&#xff0c;又怠于分割共同财产或以诉讼方式分割共同财产&#xff0c;而由债权人请求代替债务人向其他共有人提出分割财产以实现债权的诉讼。债权人代位析产&#xff0c;增加了债权…...

python+nodejs+php+springboot+vue 导师双选系统

为了直观显示系统的功能&#xff0c;运用用例图这样的工具显示分析的结果。分析的导师功能如下。导师管理导师选择信息&#xff0c;管理项目&#xff0c;管理项目提交并对学员提交的项目进行指导。 为了直观显示系统的功能&#xff0c;运用用例图这样的工具显示分析的结果。分析…...

paddle2.3-基于联邦学习实现FedAVg算法

目录 1. 联邦学习介绍 2. 实验流程 3. 数据加载 4. 模型构建 5. 数据采样函数 6. 模型训练 1. 联邦学习介绍 联邦学习是一种分布式机器学习方法&#xff0c;中心节点为server&#xff08;服务器&#xff09;&#xff0c;各分支节点为本地的client&#xff08;设备&#…...

伺服丝杠系统常用运算功能块

这篇博客主要介绍伺服、丝杠系统常用的运算功能块,其它相关运算可以查看下面文章链接: 信捷PLC脉冲频率、位移、转速相关计算(C语言编程应用)_RXXW_Dor的博客-CSDN博客里工业控制张力控制无处不在,也衍生出很多张力控制专用控制器,磁粉制动器等,本篇博客主要讨论PLC的张力…...

【Vue】模板语法,事件处理器及综合案例、自定义组件、组件通信

一、事件处理器 我们之前事件监听可以使用v-on 指令 1、事件修饰符 在Vue中我们通过由点(.)表示的指令后缀来调用修饰符&#xff0c;比如&#xff1a; .stop&#xff1a;阻止事件冒泡。当事件触发时&#xff0c;该修饰符将停止事件进一步冒泡到父元素。相当于调用了 event.stop…...

从0开始写中国象棋-创建棋盘与棋子

从控制台版本开始 考虑到象棋程序&#xff0c;其实就是数据结构与算法实现。 所以和界面相关的QT部分我们先放一放。 我们从控制台版本开始。这样大家更容易接受&#xff0c;也不影响开发。 后面我们会把控制台嫁接到QT上完成完整的游戏&#xff0c;那时候自然就水到渠成了…...

软件的开发步骤,需求分析,开发环境搭建,接口文档 ---苍穹外卖1

目录 项目总览 开发准备 开发步骤 角色分工 软件环境 项目介绍 产品原型 技术选型 开发环境搭建 前端:默认已有 后端 使用Git版本控制 数据库环境搭建 前后端联调 ​登录功能完善 导入接口文档 使用swagger​ 和yapi的区别 常用注解 项目总览 开发准备 开发步骤…...

Qt扫盲-QSqlQueryModel理论总结

QSqlQueryModel理论总结 一、概述二、使用1. 与 view 视图 绑定2. 分离视图&#xff0c;只存数据 一、概述 QSqlQueryModel是用于执行SQL语句和遍历结果集的高级接口。它构建在较低级的 QSqlQuery之上&#xff0c;可用于向QTableView 等视图类提供数据&#xff0c;也是使用了Q…...