一、OPC初识
1、OPC(OLE for Process Control),是一个工业标准,管理这个标准的国际组织是OPC基金会;
2、OPC通信结构:是指包含一个或多个OPC客户端与服务器相互通信的集合。以下是一个简单的流程图:标准的C/S结构
3、OPC服务器:TOPC基金会定义了四种;
OPC数据访问服务器,他基于OPC数据访问规范,是一种为实时数据通讯特别定义的服务器类型;
OPC历史数据访问服务器,基于OPC历史数据访问规范,是用来给支持OPC历史数据访问规范的客户端供给历史数据的服务器;
OPC报警与事件服务器:它基于OPC报警与事件访问规范,为支持OPC报警与事件规范的客户端传送报警与时间信息;
OPC UA服务器:它是基于OPC基金总会最新并且最先进的UA规范,使得OPC服务器可以与任何数据形式兼容。
4、中国的OPC基金协会官网:https://www.matrikonopc.cn
//
二、关于最新的OPC数据规范:OPC UA
1、OPC UA 涵盖了实时数据访问规范 (OPC DA)、OPC历史数据访问规范 (OPC HDA)、 OPC 报警事件访问规范 (OPC A&E) 和OPC安全协议 (OPC Security) 的不同方面, 但在其基础之上进行了功能扩展。
2、OPC UA的几大优势: 与平台无关,可在任何操作系统上运行、 为未来的先进系统做好准备,与保留系统继续兼容、 配置和维护更加方便、 基于服务的技术、 可见性增加、 通信范围更广、 通信性能提高。
///
三、基于开源项目的OPC UA在linux下的实现
参考博客:https://blog.csdn.net/qq_37887537/article/details/83510104
开源项目网址(github): https://open62541.org/
注意:open62541的1.0以上版本提供的API调用方式较之前有较大的修改,且测试后发现存在缺乏安全链接策略的bug。
open6254编程环境的搭建说明:https://blog.csdn.net/mikasoi/article/details/84799078
1、open6254在linux操作系统上的环境搭建:
1、从github上下载open6341的开源zip包,我使用的是0.3版本;
2、解压到linux系统上指定的目录上,cd ./open62541-xx(xx是版本号)
3、在Ubuntu或Debian上使用cmake工具来构建,先下载cmake工具包:
4、cd指令进入开源包解压后的目录,mkdir创建以个目录,命名为build,并cd进入;
5、分别指令cmake、make;
6、ccmake指令弹出附加编译选项选择界面,在里面可以选择编译出.so动态链接库文件和open6254.c和.h文件,可以提供API接口实现自己的OPC服务器与客户端,选择后make;
构建选项的介绍请参考链接内的说明,更加详细。
7、之后,make doc和make doc_pdf创建文档,这一步即使失败了也不影响后期的开发。
8、当你构建好这个环境后,就可以在build这个目录下实现自己的服务器与客户端了,编译后可以自由移植,这是我的测试代码:
服务器测试例子: #include <signal.h> #include <stdio.h> #include "open62541.h" UA_Boolean runing = true; static void stopHandler(int sig) { UA_LOG_INFO(UA_Log_Stdout,UA_LOGCATEGORY_USERLAND,"received ctrl c"); runing = false; } static void addVariable(UA_Server *server) { /*Define the attribute of the myInteger variable node*/ UA_VariableAttributes attr = UA_VariableAttributes_default; UA_Int32 myInteger = 43; UA_Variant_setScalar(&attr.value,&myInteger,&UA_TYPES[UA_TYPES_INT32]); attr.description = UA_LOCALIZEDTEXT("en-US","the answer"); attr.displayName = UA_LOCALIZEDTEXT("en-US","the answer"); attr.dataType = UA_TYPES[UA_TYPES_INT32].typeId; attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE; /*Add the variable node to the information model*/ UA_NodeId myIntegerNodeId = UA_NODEID_STRING(1,"the.answer"); UA_QualifiedName myIntegerName = UA_QUALIFIEDNAME(1,"the answer"); UA_NodeId parentNodeId = UA_NODEID_NUMERIC(0,UA_NS0ID_OBJECTSFOLDER); UA_NodeId parentReferenceNodeId = UA_NODEID_NUMERIC(0,UA_NS0ID_ORGANIZES); UA_Server_addVariableNode(server,myIntegerNodeId,parentNodeId,\ parentReferenceNodeId,myIntegerName,\ UA_NODEID_NUMERIC(0,UA_NS0ID_BASEDATAVARIABLETYPE),\ attr,NULL,NULL); } int main(void) { signal(SIGINT,stopHandler); signal(SIGTERM,stopHandler); UA_ServerConfig *config = UA_ServerConfig_new_default(); UA_Server *server = UA_Server_new(config); addVariable(server); UA_StatusCode retval = UA_Server_run(server,&runing); UA_Server_delete(server); UA_ServerConfig_delete(config); return (int)retval; } 客户端测试例子: #include <stdio.h> #include "open62541.h" int main(void) { UA_Client *client = UA_Client_new(UA_ClientConfig_default); UA_StatusCode retval = UA_Client_connect(client,"opc.tcp://127.0.0.1:4840"); if(retval != UA_STATUSCODE_GOOD) { UA_Client_delete(client); printf("Error is %d\n",(int)retval); return (int)retval; } /*read the value attribute of the node.UA_Client_readValueAttribute is a wrapper for the raw read service available as UA_Client_Service_read.*/ UA_Variant value; UA_Variant_init(&value); UA_NodeId testvar = UA_NODEID_STRING(1,"the.answer"); retval = UA_Client_readValueAttribute(client, testvar, &value); if(retval == UA_STATUSCODE_GOOD) { UA_Int32 *p = (UA_Int32 *)value.data; printf("Var = %d \n",*p); } /*Clean up */ UA_Variant_deleteMembers(&value); UA_Client_delete(client); return UA_STATUSCODE_GOOD; }2、代码编译指令:
gcc -o myServer ./open62541.c ./MyServer.c
gcc -o myClient ./open62541.c ./MyClient.c
/
open62541在window上的安装 参考的博客:https://blog.csdn.net/hanmingjunv5/article/details/108486016 第一步: 请下载并安装 Visual Studio 2019 Community或2017版本的,通过官网直接下载即可 第二步: 下载Cmake window版本工具,编译open62541使用(https://cmake.org/) 第三步: 下载open62541的源码包,源码包的zip打包。 第四步: 这里解压了cmake下载的包后,在bin目录里,有个cmake_gui.exe,双击打开就是下图了 先Configare,然后成功后, UA_ENABLE_AMALGAMATION=ON(打勾);然后点击Generate。 第五步: 编译成功后,打开编译好的指定路径,使用Visual Studio打开xxx.sln,进行编译出open62541.h和.c就可以了。 4、在pc端的一个免费的opc客户端软件 UaExpert 参考博客: https://www.cnblogs.com/water-sea/p/12863111.html 这个软件的可以运行在linux下,也有运行在window 32位系统上的。安装包已经上传资源,请自行下载哈:( https://download.csdn.net/download/weixin_40639467/12873453) 使用步骤:。。。。。。。。。。。。。。。。。。。。。。。 第一步:双击打开软件,你将会看到如下界面: 主要是Subject的内容,除了默认的内容,其他的全英文数字即可。之后直接点击OK。 第二步:进入主界面后,左边工程显示栏“Project”中,鼠标右键点击“Servver”,选择ADD,将弹出一个添加服务器的对话框 添加好后,点击ok关闭对象框; 第三步:回到主窗口,鼠标选中新添加的Server,点击功能菜单栏上的插座按钮,连接到服务器;如果链接成功,主窗口的 Address Space下面会出现服务器的描述,包含服务器的数据节点,可以拖动数据节点到 Data Accress View中进行监控。 /3、open62541支持的所有基本数据类型:
1、布尔变量:typedef bool UA_Boolean;
2、SByte(char):typedef int8_t UA_SByte;
3、字节(unsigned char):typedef uint8_t UA_Byte
4、INT16:typedef int16_t UA_Int16
5、UINT16:typedef uint16_t UA_UInt16
6、INT32:typedef int32_t UA_Int32
7、UINT32:typedef uint32_t UA_UInt32
8、INT64:typedef int64_t UA_Int64
9、UINT64:typedef uint64_t UA_UInt64
10、单精度浮点数:typedef float UA_Float
11、双精度浮点数:typedef double UA_Double
12、StatusCode(状态字);typedef uint32_t UA_StatusCode
13、字符串:
typedef struct {
size_t length; /* The length of the string */
UA_Byte *data; /* The content (not null-terminated) */
} UA_String
14、日期时间:typedef int64_t UA_DateTime(从1601年1月1日开始经历的纳秒数)
15、GUID(可作为全局唯一标识符):
typedef struct {
UA_UInt32 data1;
UA_UInt16 data2;
UA_UInt16 data3;
UA_Byte data4[8];
} UA_Guid
16、字节字符串(一系列8位字节):typedef UA_String UA_ByteString;
17、XmlElement(XML元素):typedef UA_String UA_XmlElement
18、ExpandedNodeid(在Nodeid基础上允许执行URI):
typedef struct {
UA_NodeId nodeId;
UA_String namespaceUri;
UA_UInt32 serverIndex;
} UA_ExpandedNodeId;
19、QualifiedName(有命名空间限定的名称):
typedef struct {
UA_UInt16 namespaceIndex;
UA_String name;
} UA_QualifiedName;
20、LocalizedText ( 具有可选区域设置标识符的人类可读文本):
typedef struct {
UA_String locale;
UA_String text;
} UA_LocalizedText;
21、NumericRange ( 指示(多维)数组的子集):
typedef struct {
UA_UInt32 min;
UA_UInt32 max;
} UA_NumericRangeDimension;
typedef struct {
size_t dimensionsSize;
UA_NumericRangeDimension *dimensions;
} UA_NumericRange;
///
四、基于Open62541开源库的客户端开发 ( 简单流程 ) 这里我事先在Ubuntu上搭建好open62541的开发环境,对open62541进行的二次封装;封装出来的函数接口可以快速开发针对下面我自己封装的服务器的客户端,同样支持传输的数据类型为BOOL、BYTE、INT、DINT、UINT、UDINT。 以下将对客户端源码进行分析: (完整的代码已经上传,请需要的朋友下载哈: https://download.csdn.net/download/weixin_40639467/12804246 ) 1、结构体:OPC_CLIENT 这个是我自己封装的结构体,在创建客户端之前,需要先创建并配置一个OPC_CLIENT对象;主要是指明客户端连接的服务器ip地址与登陆用户等信息: typedef struct opcua_client_struct{ UA_Client *Client; //这个无需配置,客户端创建成功后会被自动赋值// unsigned int ClientPort; //暂时未用,仅用来区分不同客户端// /*目标服务器配置信息*/ char *ServerIP; unsigned int ServerPort; char *Username; //如果为NULL,则自动匿名登陆// char *Password; }OPC_CLIENT; 2、调用函数 int Connect_Opcua_Server(OPC_CLIENT *opc_client)根据配置创建客户端并连接远程服务器: 这里基于源码,讲解open62541的原理 源码片段: /*1 判断各种配置参数是否正确*/ if(NULL == (opc_client->ServerIP)) return -2; if(0 >= (opc_client->ServerPort)) return -3; /*2 开始组装目标服务器完整域名和登陆权限信息*/ char * Server_name = malloc(sizeof(char)*64); if(NULL == Server_name) return -4; memset( Server_name,0,sizeof(char)*64); sprintf( Server_name,"opc.tcp://%s:%u",(opc_client->ServerIP),(opc_client->ServerPort)); //组装登陆目标服务器需要的标准的字符串指令,指令格式是open62541规定的:opc.tcp://xxx.xxx.xxx.xxx:portid // /*3 开始创建客户端并连接服务器*/ opc_client->Client = UA_Client_new( UA_ClientConfig_default); //调用open62541提供的函数接口UA_Client_new来使用默认配置选项生成一个客户端对象// if(NULL == (opc_client->Client)) { free(Server_name); return -5; } UA_StatusCode retvar; if(NULL == (opc_client->Username)) //匿名登陆远程服务器// retvar = UA_Client_connect((opc_client->Client),Server_name); //这个函数接口是open62541提供的用于匿名登陆服务器的// else retvar = UA_Client_connect_username((opc_client->Client),Server_name,(opc_client->Username),(opc_client->Password)); //这个函数接口是open62541提供的用来非匿名登陆服务器的// if(retvar != UA_STATUSCODE_GOOD) { UA_Client_delete(opc_client->Client); opc_client->Client = NULL; free(Server_name); return -6; } free(Server_name); return 0; } 解析: 1、 创建一个客户端对象 UA_Client UA_EXPORT * UA_Client_new(UA_ClientConfig config) 由open62541提供的功能函数接口,会根据传入参数 UA_ClientConfig来生成一个客户端对象: UA_Client * UA_Client_new( UA_ClientConfig config) { UA_Client *client = (UA_Client*)UA_malloc(sizeof(UA_Client)); //1 动态创建一个结构体对象,UA_Client// if(!client) return NULL; UA_Client_init(client, config); //2 初始化这个对象,使用了传入的参数// return client; } typedef struct UA_ClientConfig { UA_UInt32 timeout; /* ASync + Sync response timeout in ms */ UA_UInt32 secureChannelLifeTime; /* Lifetime in ms (then the channel needs to be renewed) */ UA_Logger logger; UA_ConnectionConfig localConnectionConfig; UA_ConnectClientConnection connectionFunc; /* Custom DataTypes */ size_t customDataTypesSize; const UA_DataType *customDataTypes; /* Callback function */ UA_ClientStateCallback stateCallback; #ifdef UA_ENABLE_SUBSCRIPTIONS /* When outStandingPublishRequests is greater than 0, * the server automatically create publishRequest when * UA_Client_runAsync is called. If the client don't receive * a publishResponse after : * (sub->publishingInterval * sub->maxKeepAliveCount) + * client->config.timeout) * then, the client call subscriptionInactivityCallback * The connection can be closed, this in an attempt to * recreate a healthy connection. */ UA_SubscriptionInactivityCallback subscriptionInactivityCallback; #endif /* When connectivityCheckInterval is greater than 0, * every connectivityCheckInterval (in ms), a async read request * is performed on the server. inactivityCallback is called * when the client receive no response for this read request * The connection can be closed, this in an attempt to * recreate a healthy connection. */ UA_InactivityCallback inactivityCallback; void *clientContext; #ifdef UA_ENABLE_SUBSCRIPTIONS /* number of PublishResponse standing in the sever * 0 = background task disabled */ UA_UInt16 outStandingPublishRequests; #endif /* connectivity check interval in ms * 0 = background task disabled */ UA_UInt32 connectivityCheckInterval; } UA_ClientConfig; const UA_ClientConfig UA_ClientConfig_default = { 5000, /* .timeout, 5 seconds */ 10 * 60 * 1000, /* .secureChannelLifeTime, 10 minutes */ UA_Log_Stdout, /* .logger */ { /* .localConnectionConfig */ 0, /* .protocolVersion */ 65535, /* .sendBufferSize, 64k per chunk */ 65535, /* .recvBufferSize, 64k per chunk */ 0, /* .maxMessageSize, 0 -> unlimited */ 0 /* .maxChunkCount, 0 -> unlimited */ }, UA_ClientConnectionTCP, /* .connectionFunc ,这里是配置客户端基于TCPsocket链接服务器,一个函数指针*/ 0, /* .customDataTypesSize */ NULL, /*.customDataTypes */ NULL, /*.stateCallback */ #ifdef UA_ENABLE_SUBSCRIPTIONS NULL, /*.subscriptionInactivityCallback */ #endif NULL, /*.inactivityCallback */ NULL, /*.clientContext */ #ifdef UA_ENABLE_SUBSCRIPTIONS 10, /* .outStandingPublishRequests */ #endif 0 /* .connectivityCheckInterval */ }; //默认配置客户端的配置内容// static void UA_Client_init(UA_Client* client, UA_ClientConfig config) { memset(client, 0, sizeof(UA_Client)); /* TODO: Select policy according to the endpoint ,根据端点选择数据传输的安全策略*/ UA_SecurityPolicy_None(&client->securityPolicy, NULL, UA_BYTESTRING_NULL, config.logger); client->channel.securityPolicy = &client->securityPolicy; client->channel.securityMode = UA_MESSAGESECURITYMODE_NONE; //默认不加密// client->config = config; if(client->config.stateCallback) client->config.stateCallback(client, client->state); } 2、 客户端连接服务器的函数: UA_StatusCode UA_EXPORT UA_Client_connect(UA_Client *client, const char *endpointUrl) 和 UA_StatusCode UA_EXPORT UA_Client_connect_username(UA_Client *client, const char *endpointUrl,const char *username, const char *password) 是open62541提供的,客户端登陆远程目标服务器的两个不同方式: UA_StatusCode UA_Client_connect(UA_Client *client, const char *endpointUrl) { return UA_Client_connectInternal(client, endpointUrl, UA_TRUE, UA_TRUE); //这个函数最终会调用TCP链接的函数接口来创建链接// } UA_StatusCode UA_Client_connect_username(UA_Client *client, const char *endpointUrl, const char *username, const char *password) { client->authenticationMethod = UA_CLIENTAUTHENTICATION_USERNAME; //这里配置为需要用户密码的模式// client->username = UA_STRING_ALLOC(username); client->password = UA_STRING_ALLOC(password); return UA_Client_connect(client, endpointUrl); //其实最终也要调用connect函数// } 3、调用函数:int Readdata_by_Client 从客户端所连接的服务器上读取指定的某个数据 源码片段: UA_StatusCode retvar; UA_Variant Values; UA_Variant_init(&Values); UA_NodeId Data_node; /*根据不同数据类型,读取远程服务器的不同数据节点*/ switch(Data_Type) { case 0: //READ BOOL TYPE// { Data_node = UA_NODEID_STRING(1,"JS.BOOL"); //这里配置获取服务器数据节点的标识,返回服务器数据节点对象// retvar = UA_Client_readValueAttribute((opc_client->Client), Data_node,& Values); //调用open62541提供的函数接口获取数据,存放在open62541统一的数据对象结构体UA_Variant内// if(retvar != UA_STATUSCODE_GOOD) return -3; UA_Boolean *Read_Array; Read_Array = Values.data; *((UA_Boolean*)Data_Value) = Read_Array[Data_Num]; break; } case 1: //READ BYTE TYPE// { Data_node = UA_NODEID_STRING(1,"JS.BYTE"); retvar = UA_Client_readValueAttribute((opc_client->Client),Data_node,&Values); if(retvar != UA_STATUSCODE_GOOD) return -3; UA_Byte *Read_Array; Read_Array = Values.data; *((UA_Byte*)Data_Value) = Read_Array[Data_Num]; break; } case 2: //READ INT TYPE// { Data_node = UA_NODEID_STRING(1,"JS.INT"); retvar = UA_Client_readValueAttribute((opc_client->Client),Data_node,&Values); if(retvar != UA_STATUSCODE_GOOD) return -3; UA_Int16 *Read_Array; Read_Array = Values.data; *((UA_Int16*)Data_Value) = Read_Array[Data_Num]; break; } case 3: //READ UINT TYPE// { Data_node = UA_NODEID_STRING(1,"JS.UINT"); retvar = UA_Client_readValueAttribute((opc_client->Client),Data_node,&Values); if(retvar != UA_STATUSCODE_GOOD) return -3; UA_UInt16 *Read_Array; Read_Array = Values.data; *((UA_UInt16*)Data_Value) = Read_Array[Data_Num]; break; } case 4: //READ DINT TYPE// { Data_node = UA_NODEID_STRING(1,"JS.DINT"); retvar = UA_Client_readValueAttribute((opc_client->Client),Data_node,&Values); if(retvar != UA_STATUSCODE_GOOD) return -3; UA_Int32 *Read_Array; Read_Array = Values.data; *((UA_Int32*)Data_Value) = Read_Array[Data_Num]; break; } case 5: //READ UDINT TYPE// { Data_node = UA_NODEID_STRING(1,"JS.UDINT"); retvar = UA_Client_readValueAttribute((opc_client->Client),Data_node,&Values); if(retvar != UA_STATUSCODE_GOOD) return -3; UA_UInt32 *Read_Array; Read_Array = Values.data; *((UA_UInt32*)Data_Value) = Read_Array[Data_Num]; break; } } 解析: 1、 读取服务器数据节点数据函数: static UA_INLINE UA_StatusCode UA_Client_readValueAttribute(UA_Client *client, const UA_NodeId nodeId,UA_Variant *outValue) 是open62541提供的函数接口: static UA_INLINE UA_StatusCode UA_Client_readValueAttribute(UA_Client *client, const UA_NodeId nodeId, UA_Variant *outValue) { return __UA_Client_readAttribute(client, &nodeId, UA_ATTRIBUTEID_VALUE, outValue, &UA_TYPES[UA_TYPES_VARIANT]); } UA_StatusCode __UA_Client_readAttribute(UA_Client *client, const UA_NodeId *nodeId, UA_AttributeId attributeId, void *out, const UA_DataType *outDataType) { UA_ReadValueId item; UA_ReadValueId_init(&item); item.nodeId = *nodeId; item.attributeId = attributeId; UA_ReadRequest request; UA_ReadRequest_init(&request); request.nodesToRead = &item; request.nodesToReadSize = 1; UA_ReadResponse response = UA_Client_Service_read(client, request); UA_StatusCode retval = response.responseHeader.serviceResult; if(retval == UA_STATUSCODE_GOOD) { if(response.resultsSize == 1) retval = response.results[0].status; else retval = UA_STATUSCODE_BADUNEXPECTEDERROR; } if(retval != UA_STATUSCODE_GOOD) { UA_ReadResponse_deleteMembers(&response); return retval; } /* Set the StatusCode */ UA_DataValue *res = response.results; if(res->hasStatus) retval = res->status; /* Return early of no value is given */ if(!res->hasValue) { if(retval == UA_STATUSCODE_GOOD) retval = UA_STATUSCODE_BADUNEXPECTEDERROR; UA_ReadResponse_deleteMembers(&response); return retval; } /* Copy value into out */ if(attributeId == UA_ATTRIBUTEID_VALUE) { memcpy(out, &res->value, sizeof(UA_Variant)); UA_Variant_init(&res->value); } else if(attributeId == UA_ATTRIBUTEID_NODECLASS) { memcpy(out, (UA_NodeClass*)res->value.data, sizeof(UA_NodeClass)); } else if(UA_Variant_isScalar(&res->value) && res->value.type == outDataType) { memcpy(out, res->value.data, res->value.type->memSize); UA_free(res->value.data); res->value.data = NULL; } else { retval = UA_STATUSCODE_BADUNEXPECTEDERROR; } UA_ReadResponse_deleteMembers(&response); return retval; } 4、调用函数 int Writedata_by_Client 将数据通过客户端写入到目标服务器的数据节点中 源码片段: UA_StatusCode retvar; UA_Variant Values; UA_Variant_init(&Values); /*修改服务器数据,需要先将数据读取出来后,修改再重新写入*/ switch(Data_Type) { case 0: //WRITE BOOL TYPE// { UA_NodeId Data_node = UA_NODEID_STRING(1,"JS.BOOL"); //指定要操作的目标服务器的数据节点标识,返回数据节点描述对象// retvar = UA_Client_readValueAttribute((opc_client->Client), Data_node,& Values); //先从数据节点中读取数据同步到客户端// if(retvar != UA_STATUSCODE_GOOD) return -3; UA_Boolean *Data_Array = Values.data; Data_Array[Data_Num] = *((UA_Boolean*)Data_Value); retvar = UA_Client_writeValueAttribute((opc_client->Client), Data_node,& Values); //修改读取到的历史数据中,想要更新的部分,再次写回服务器// if(retvar != UA_STATUSCODE_GOOD) return -3; break; } case 1: //WRITE BYTE TYPE// { UA_NodeId Data_node = UA_NODEID_STRING(1,"JS.BYTE"); retvar = UA_Client_readValueAttribute((opc_client->Client),Data_node,&Values); if(retvar != UA_STATUSCODE_GOOD) return -3; UA_Byte *Data_Array = Values.data; Data_Array[Data_Num] = *((UA_Byte*)Data_Value); retvar = UA_Client_writeValueAttribute((opc_client->Client),Data_node,&Values); if(retvar != UA_STATUSCODE_GOOD) return -3; break; } case 2: //WRITE INT TYPE// { UA_NodeId Data_node = UA_NODEID_STRING(1,"JS.INT"); retvar = UA_Client_readValueAttribute((opc_client->Client),Data_node,&Values); if(retvar != UA_STATUSCODE_GOOD) return -3; UA_Int16 *Data_Array = Values.data; Data_Array[Data_Num] = *((UA_Int16*)Data_Value); retvar = UA_Client_writeValueAttribute((opc_client->Client),Data_node,&Values); if(retvar != UA_STATUSCODE_GOOD) return -3; break; } case 3: //WRITE UINT TYPE// { UA_NodeId Data_node = UA_NODEID_STRING(1,"JS.UINT"); retvar = UA_Client_readValueAttribute((opc_client->Client),Data_node,&Values); if(retvar != UA_STATUSCODE_GOOD) return -3; UA_UInt16 *Data_Array = Values.data; Data_Array[Data_Num] = *((UA_UInt16*)Data_Value); retvar = UA_Client_writeValueAttribute((opc_client->Client),Data_node,&Values); if(retvar != UA_STATUSCODE_GOOD) return -3; break; } case 4: //WRITE DINT TYPE// { UA_NodeId Data_node = UA_NODEID_STRING(1,"JS.DINT"); retvar = UA_Client_readValueAttribute((opc_client->Client),Data_node,&Values); if(retvar != UA_STATUSCODE_GOOD) return -3; UA_Int32 *Data_Array = Values.data; Data_Array[Data_Num] = *((UA_Int32*)Data_Value); retvar = UA_Client_writeValueAttribute((opc_client->Client),Data_node,&Values); if(retvar != UA_STATUSCODE_GOOD) return -3; break; } case 5: //WRITE UDINT TYPE// { UA_NodeId Data_node = UA_NODEID_STRING(1,"JS.UDINT"); retvar = UA_Client_readValueAttribute((opc_client->Client),Data_node,&Values); if(retvar != UA_STATUSCODE_GOOD) return -3; UA_UInt32 *Data_Array = Values.data; Data_Array[Data_Num] = *((UA_UInt32*)Data_Value); retvar = UA_Client_writeValueAttribute((opc_client->Client),Data_node,&Values); if(retvar != UA_STATUSCODE_GOOD) return -3; break; } } 解析: 1、 写数据到服务器函数接口: static UA_INLINE UA_StatusCode UA_Client_writeValueAttribute(UA_Client *client, const UA_NodeId nodeId,const UA_Variant *newValue) 是open62541提供的用于客户端写数据到服务器的接口 static UA_INLINE UA_StatusCode UA_Client_writeValueAttribute(UA_Client *client, const UA_NodeId nodeId, const UA_Variant *newValue) { return __UA_Client_writeAttribute(client, &nodeId, UA_ATTRIBUTEID_VALUE, newValue, &UA_TYPES[UA_TYPES_VARIANT]); } UA_StatusCode __UA_Client_writeAttribute(UA_Client *client, const UA_NodeId *nodeId, UA_AttributeId attributeId, const void *in, const UA_DataType *inDataType) { if(!in) return UA_STATUSCODE_BADTYPEMISMATCH; UA_WriteValue wValue; UA_WriteValue_init(&wValue); wValue.nodeId = *nodeId; wValue.attributeId = attributeId; if(attributeId == UA_ATTRIBUTEID_VALUE) wValue.value.value = *(const UA_Variant*)in; else /* hack. is never written into. */ UA_Variant_setScalar(&wValue.value.value, (void*)(uintptr_t)in, inDataType); wValue.value.hasValue = true; UA_WriteRequest wReq; UA_WriteRequest_init(&wReq); wReq.nodesToWrite = &wValue; wReq.nodesToWriteSize = 1; UA_WriteResponse wResp = UA_Client_Service_write(client, wReq); UA_StatusCode retval = wResp.responseHeader.serviceResult; if(retval == UA_STATUSCODE_GOOD) { if(wResp.resultsSize == 1) retval = wResp.results[0]; else retval = UA_STATUSCODE_BADUNEXPECTEDERROR; } UA_WriteResponse_deleteMembers(&wResp); return retval; } 5 、调用函数 void Disconnect_Opcua_Server(OPC_CLIENT *opc_client) 断开客户端与服务器的链接并释放客户端资源 源码片段: if(NULL == opc_client) return; if(NULL != (opc_client->Client)) UA_Client_delete(opc_client->Client); //调用open62541提供的函数接口即可自动断开连接并删除客户端// opc_client->Client = NULL; 解析: 1、 关闭客户端函数接口 void UA_EXPORT UA_Client_delete(UA_Client *client) 是open62541提供用来很方便指示客户端断开连接并自动回收资源的一个函数 void UA_Client_delete(UA_Client* client) { UA_Client_deleteMembers(client); UA_free(client); //这里相当于free// } static void UA_Client_deleteMembers(UA_Client* client) { UA_Client_disconnect(client); //断开连接。。。。。。。。。// client->securityPolicy.deleteMembers(&client->securityPolicy); //删除客户端安全策略配置// UA_SecureChannel_deleteMembersCleanup(&client->channel); UA_Connection_deleteMembers(&client->connection); if(client->endpointUrl.data) UA_String_deleteMembers(&client->endpointUrl); UA_UserTokenPolicy_deleteMembers(&client->token); UA_NodeId_deleteMembers(&client->authenticationToken); if(client->username.data) UA_String_deleteMembers(&client->username); if(client->password.data) UA_String_deleteMembers(&client->password); /* Delete the async service calls */ UA_Client_AsyncService_removeAll(client, UA_STATUSCODE_BADSHUTDOWN); /* Delete the subscriptions */ #ifdef UA_ENABLE_SUBSCRIPTIONS UA_Client_Subscriptions_clean(client); #endif } // // 五、基于Open62541开源的服务器开发 ( 简单流程 ) 这里我基于事先搭建好的开发环境,对open62541.c进行二次封装,封装出的函数接口可以快速搭建数据传输的OPC UA tcp服务器。服务器支持用户密码登陆,支持传输的数据类型有BOOL、BYTE、INT、DINT、UINT、UDINT。 以下是根据服务器对原代码的分析 : (完整的代码已经上传,请需要的朋友下载哈: https://download.csdn.net/download/weixin_40639467/12804229 ) 1、结构体: OPC_SERVER 这个是我自己封装的结构体,在创建服务器之前,需要先申明一个OPC_SERVER结构体对象,并对其中的一些成员变量进行配置: typedef struct opcua_server_struc t { UA_ServerConfig *config; //服务器描述结构体对象,内部自动赋值// UA_Server *Server; //创建服务器成功后,会被自动赋予一个对象// UA_Boolean Enable; //1=启动服务器,0=关闭服务器// UA_UInt16 PortID; //配置服务器使用的端口号,实际上不一定要4840// char *IPV4_ADDR; //配置服务器使用的IPV4地址,如果为NULL则系统会自动寻找可用ip// /*服务器账户密码*/ UA_Boolean Anonymous; //true=允许用户匿名登陆,False=禁止匿名用户登录// char *Username; //配置服务器的登陆用户名,无论是否允许匿名登陆,这里不能为NULL!!// char *Passwer; //配置服务器的登陆密码,也是不能为NULL// /*服务器内存数据空间*/ unsigned int bool_length,byte_length,int_length, dint_length,uint_length,udint_length; //服务器支持的各种数据类型的数据个数,理论上没有上限,只要系统内存足够// UA_Boolean *bool_addr; //服务器BOOL类型数据所在的内存首地址// UA_Byte *byte_addr; //服务器BYTE类型数据所在的内存首地址// UA_Int16 *int_addr; UA_Int32 *dint_addr; UA_UInt16 *uint_addr; UA_UInt32 *udint_addr; UA_Boolean NodeID_Read[6]; //按位记录远端客户端读取服务器数据节点的请求状态,第0位被置1表示有客户端请求读取服务器BOOL类型数据,这个状态位无需自动清零,后续调用写函数会自动清零// UA_Boolean NodeID_Write[6]; //按位记录远端客户端写入服务器数据节点的请求状态,也无需手动清零// } OPC_SERVER; 2、调用函数创建opc ua服务器:int Create_opcua_server( OPC_SERVER *opc_server ): 这里将逐步分析配置流程,目的是讲解open62541的代码实现原理。 第一步 向内存申请一个服务器对象 源码片段: opc_server->config = UA_ServerConfig_new_default(opc_server->PortID); if(NULL == (opc_server->config)) return -2; if(NULL != (opc_server->IPV4_ADDR)) //如果为NULL则默认自动配置// UA_ServerConfig_set_customHostname((opc_server->config),UA_String_fromChars(opc_server->IPV4_ADDR)); //指定使用的IPV4地址// if((NULL == (opc_server->Username)) || (NULL == (opc_server->Passwer))) { UA_ServerConfig_delete(opc_server->config); opc_server->config = NULL; return -3; } 解析: 1、 UA_ServerConfig_new_default(void) 是open62541提供的用于生成使用默认配置的服务器配置对象的函数接口, 这里我做了修改,允许自定义网络端口号:( 在open62541.h中被定义) /* Creates a server config on the default port 4840 with no server * certificate. */ static UA_INLINE UA_ServerConfig * UA_ServerConfig_new_default( UA_UInt16 portNumber ) { return UA_ServerConfig_new_minimal( portNumber , NULL); //这里调用了open62541.c的核心函数,也是真正实现配置的接口// } 2、 UA_ServerConfig_set_customHostname( UA_ServerConfig *config, const UA_String customHostname ) 是open62541提供的用于允许手动指定服务器ipv4地址的函数接口,在open62541.h中被定义,在open62541.c中实现: void UA_ServerConfig_set_customHostname(UA_ServerConfig *config, const UA_String customHostname) { if(!config) return; UA_String_deleteMembers(&config->customHostname); UA_String_copy(&customHostname, &config->customHostname); } 3、 UA_ServerConfig_delete( UA_ServerConfig *config ) 是用来删除服务器配置对象的函数接口,由open62541提供;服务器对象在open62541中是调用malloc动态分配的,所以必须在不需要的时候手动释放资源。 第二步 配置服务器的登陆账户与密码 源码片段 : ret_var = Set_UserPasswer(opc_server,opc_server->Anonymous); //这里调用了内部实现函数 Set_UserPasswer,我自己的内部函数 // if(0>ret_var) { UA_ServerConfig_delete(opc_server->config); opc_server->config = NULL; return -4; } static int Set_UserPasswer(OPC_SERVER *opc_server,UA_Boolean Anonymous) //配置服务器用户密码认证// { if(NULL == opc_server) return -1; UA_ServerConfig *config = (opc_server->config); UA_UsernamePasswordLogin PasswordLogin[2] = {{{strlen((opc_server->Username)),(UA_Byte*)((opc_server->Username))},{ strlen((opc_server->Passwer)),(UA_Byte*)((opc_server->Passwer))}},{ UA_STRING_STATIC("Administrator"),UA_STRING_STATIC("administrator")}}; //定义设置服务器用户和密码的数组// config->accessControl = UA_AccessControl_default(Anonymous,2,PasswordLogin); //配置服务器用户密码,可以有多个,这里配置了2个, 其中一个作为管理员被内置 // return 0; } 解析: 1、 变量 UA_UsernamePasswordLogin 是open62541提供的一个结构体变量,结构体变量用来描述一个服务器的用户及其登陆密码,opc ua的一个安全策略就是客户端访问服务器可以受到用户密码的管理; 一个服务器可以被多个使用相同用户登录的客户端访问。 typedef struct { UA_String username; UA_String password; } UA_UsernamePasswordLogin; 2、 UA_AccessControl UA_AccessControl_default( UA_Boolean allowAnonymous, size_t usernamePasswordLoginSize, const UA_UsernamePasswordLogin *usernamePasswordLogin ) 是open62541提供的函数接口,用来配置并返回一个服务器用户登录管理对象,open62541默认配置的服务器对应不允许匿名登陆,如果需要自由定义,通过调用这个函数接口可以修改。 UA_AccessControl UA_AccessControl_default(UA_Boolean allowAnonymous, size_t usernamePasswordLoginSize, const UA_UsernamePasswordLogin *usernamePasswordLogin) { AccessControlContext *context = (AccessControlContext*) UA_calloc(1, sizeof(AccessControlContext)); UA_AccessControl ac; memset(&ac, 0, sizeof(ac)); ac.context = context; ac.deleteMembers = deleteMembers_default; ac.activateSession = activateSession_default; ac.closeSession = closeSession_default; ac.getUserRightsMask = getUserRightsMask_default; ac.getUserAccessLevel = getUserAccessLevel_default; ac.getUserExecutable = getUserExecutable_default; ac.getUserExecutableOnObject = getUserExecutableOnObject_default; ac.allowAddNode = allowAddNode_default; ac.allowAddReference = allowAddReference_default; ac.allowDeleteNode = allowDeleteNode_default; ac.allowDeleteReference = allowDeleteReference_default; /* Allow anonymous? */ context->allowAnonymous = allowAnonymous; /* Copy username/password to the access control plugin */ if(usernamePasswordLoginSize > 0) { context->usernamePasswordLogin = (UA_UsernamePasswordLogin*) UA_malloc(usernamePasswordLoginSize * sizeof(UA_UsernamePasswordLogin)); if(!context->usernamePasswordLogin) return ac; context->usernamePasswordLoginSize = usernamePasswordLoginSize; for(size_t i = 0; i < usernamePasswordLoginSize; i++) { UA_String_copy(&usernamePasswordLogin[i].username, &context->usernamePasswordLogin[i].username); UA_String_copy(&usernamePasswordLogin[i].password, &context->usernamePasswordLogin[i].password); } } /* Set the allowed policies */ size_t policies = 0; if(allowAnonymous) policies++; if(usernamePasswordLoginSize > 0) policies++; ac.userTokenPoliciesSize = 0; ac.userTokenPolicies = (UA_UserTokenPolicy *) UA_Array_new(policies, &UA_TYPES[UA_TYPES_USERTOKENPOLICY]); if(!ac.userTokenPolicies) return ac; ac.userTokenPoliciesSize = policies; policies = 0; if(allowAnonymous) { ac.userTokenPolicies[policies].tokenType = UA_USERTOKENTYPE_ANONYMOUS; ac.userTokenPolicies[policies].policyId = UA_STRING_ALLOC(ANONYMOUS_POLICY); policies++; } if(usernamePasswordLoginSize > 0) { ac.userTokenPolicies[policies].tokenType = UA_USERTOKENTYPE_USERNAME; ac.userTokenPolicies[policies].policyId = UA_STRING_ALLOC(USERNAME_POLICY); /* No encryption of username/password supported at the moment */ ac.userTokenPolicies[policies].securityPolicyUri = UA_STRING_ALLOC(" http://opcfoundation.org/UA/SecurityPolicy#None"); } return ac; } 第三步 根据具体的配置,创建服务器对象 源码片段: opc_server->Server = UA_Server_new(opc_server->config); //调用open62541提供的函数接口,根据配置生成服务器对象// if(NULL == (opc_server->Server)) { UA_ServerConfig_delete(opc_server->config); opc_server->config = NULL; return -5; } 解析: 1、 服务器配置结构体对象 UA_ServerConfig 是open62541定义用来描述服务器配置的结构体变量,创建一个服务器之前,需要先生成这个对象。 struct UA_ServerConfig { UA_UInt16 nThreads; /* only if multithreading is enabled */ UA_Logger logger; /* Server Description */ UA_BuildInfo buildInfo; UA_ApplicationDescription applicationDescription; UA_ByteString serverCertificate; /* MDNS Discovery */ #ifdef UA_ENABLE_DISCOVERY UA_String mdnsServerName; size_t serverCapabilitiesSize; UA_String *serverCapabilities; #endif /* Custom DataTypes */ size_t customDataTypesSize; UA_DataType *customDataTypes; /** * .. note:: See the section on :ref:`generic-types`. Examples for working * with custom data types are provided in * ``/examples/custom_datatype/``. */ /* Nodestore */ UA_Nodestore nodestore; /* Networking */ size_t networkLayersSize; UA_ServerNetworkLayer *networkLayers; UA_String customHostname; /* Available endpoints */ size_t endpointsSize; UA_Endpoint *endpoints; /* Node Lifecycle callbacks */ UA_GlobalNodeLifecycle nodeLifecycle; /** * .. note:: See the section for :ref:`node lifecycle * handling<node-lifecycle>`. */ /* Access Control */ UA_AccessControl accessControl; /** * .. note:: See the section for :ref:`access-control * handling<access-control>`. */ /* Certificate Verification */ UA_CertificateVerification certificateVerification; /* Limits for SecureChannels */ UA_UInt16 maxSecureChannels; UA_UInt32 maxSecurityTokenLifetime; /* in ms */ /* Limits for Sessions */ UA_UInt16 maxSessions; UA_Double maxSessionTimeout; /* in ms */ /* Operation limits */ UA_UInt32 maxNodesPerRead; UA_UInt32 maxNodesPerWrite; UA_UInt32 maxNodesPerMethodCall; UA_UInt32 maxNodesPerBrowse; UA_UInt32 maxNodesPerRegisterNodes; UA_UInt32 maxNodesPerTranslateBrowsePathsToNodeIds; UA_UInt32 maxNodesPerNodeManagement; UA_UInt32 maxMonitoredItemsPerCall; /* Limits for Requests */ UA_UInt32 maxReferencesPerNode; /* Limits for Subscriptions */ UA_UInt32 maxSubscriptionsPerSession; UA_DurationRange publishingIntervalLimits; /* in ms (must not be less than 5) */ UA_UInt32Range lifeTimeCountLimits; UA_UInt32Range keepAliveCountLimits; UA_UInt32 maxNotificationsPerPublish; UA_UInt32 maxRetransmissionQueueSize; /* 0 -> unlimited size */ /* Limits for MonitoredItems */ UA_UInt32 maxMonitoredItemsPerSubscription; UA_DurationRange samplingIntervalLimits; /* in ms (must not be less than 5) */ UA_UInt32Range queueSizeLimits; /* Negotiated with the client */ /* Limits for PublishRequests */ UA_UInt32 maxPublishReqPerSession; /* Discovery */ #ifdef UA_ENABLE_DISCOVERY /* Timeout in seconds when to automatically remove a registered server from * the list, if it doesn't re-register within the given time frame. A value * of 0 disables automatic removal. Default is 60 Minutes (60*60). Must be * bigger than 10 seconds, because cleanup is only triggered approximately * ervery 10 seconds. The server will still be removed depending on the * state of the semaphore file. */ UA_UInt32 discoveryCleanupTimeout; #endif }; 2、 服务器结构体 UA_Server 是open62541定义的用来描述一个生成的服务器的对象,open62541通过哈希表来管理服务器的寄存器和网络socket,理论上同一个设备上可以创建多个服务器,使用相同的ipv4地址, 不同端口号即可: struct UA_Server { /* Meta */ UA_DateTime startTime; /* Security */ UA_SecureChannelManager secureChannelManager; UA_SessionManager sessionManager; #ifdef UA_ENABLE_DISCOVERY /* Discovery */ LIST_HEAD(registeredServer_list, registeredServer_list_entry) registeredServers; // doubly-linked list of registered servers size_t registeredServersSize; LIST_HEAD(periodicServerRegisterCallback_list, periodicServerRegisterCallback_entry) periodicServerRegisterCallbacks; // doubly-linked list of current register callbacks UA_Server_registerServerCallback registerServerCallback; void* registerServerCallbackData; # ifdef UA_ENABLE_DISCOVERY_MULTICAST mdns_daemon_t *mdnsDaemon; #ifdef _WIN32 SOCKET mdnsSocket; #else int mdnsSocket; #endif UA_Boolean mdnsMainSrvAdded; # ifdef UA_ENABLE_MULTITHREADING pthread_t mdnsThread; UA_Boolean mdnsRunning; # endif LIST_HEAD(serverOnNetwork_list, serverOnNetwork_list_entry) serverOnNetwork; // doubly-linked list of servers on the network (from mDNS) size_t serverOnNetworkSize; UA_UInt32 serverOnNetworkRecordIdCounter; UA_DateTime serverOnNetworkRecordIdLastReset; // hash mapping domain name to serverOnNetwork list entry struct serverOnNetwork_hash_entry* serverOnNetworkHash[SERVER_ON_NETWORK_HASH_PRIME]; UA_Server_serverOnNetworkCallback serverOnNetworkCallback; void* serverOnNetworkCallbackData; # endif #endif /* Namespaces */ size_t namespacesSize; UA_String *namespaces; /* Callbacks with a repetition interval */ UA_Timer timer; /* Delayed callbacks */ SLIST_HEAD(DelayedCallbacksList, UA_DelayedCallback) delayedCallbacks; /* Worker threads */ #ifdef UA_ENABLE_MULTITHREADING UA_Worker *workers; /* there are nThread workers in a running server */ UA_DispatchQueue dispatchQueue; /* Dispatch queue for the worker threads */ pthread_mutex_t dispatchQueue_accessMutex; /* mutex for access to queue */ pthread_cond_t dispatchQueue_condition; /* so the workers don't spin if the queue is empty */ pthread_mutex_t dispatchQueue_conditionMutex; /* mutex for access to condition variable */ #endif /* For bootstrapping, omit some consistency checks, creating a reference to * the parent and member instantiation */ UA_Boolean bootstrapNS0; /* Config */ UA_ServerConfig config; /* Local access to the services (for startup and maintenance) uses this * Session with all possible access rights (Session Id: 1) */ UA_Session adminSession; }; 3、 函数 UA_Server * UA_Server_new(const UA_ServerConfig *config) 是open62541提供的用来根据配置生成服务器对象的函数接口,函数的具体实现被定义在open62541.c中, 这个函数仅仅是生成服务器,服务器并没有启动: UA_Server * UA_Server_new(const UA_ServerConfig *config) { /* A config is required */ if(!config) return NULL; /* At least one endpoint has to be configured 至少配置一个端点*/ if(config->endpointsSize == 0) { UA_LOG_FATAL(config->logger, UA_LOGCATEGORY_SERVER, "There has to be at least one endpoint."); return NULL; } /* Allocate the server 配置服务器*/ UA_Server *server = (UA_Server *)UA_calloc(1, sizeof(UA_Server)); if(!server) return NULL; /* Set the config */ server->config = *config; /* Initialize the admin session 初始化管理会话*/ UA_Session_init(&server->adminSession); server->adminSession.header.authenticationToken = UA_NODEID_NUMERIC(0, 1); server->adminSession.sessionId.identifierType = UA_NODEIDTYPE_GUID; server->adminSession.sessionId.identifier.guid.data1 = 1; server->adminSession.sessionName = UA_STRING_ALLOC("Administrator Session"); server->adminSession.validTill = UA_INT64_MAX; server->adminSession.availableContinuationPoints = UA_MAXCONTINUATIONPOINTS; /* Init start time to zero, the actual start time will be sampled in * UA_Server_run_startup() */ server->startTime = 0; /* Set a seed for non-cyptographic randomness */ #ifndef UA_ENABLE_DETERMINISTIC_RNG UA_random_seed((UA_UInt64)UA_DateTime_now()); #endif /* Initialize the handling of repeated callbacks */ UA_Timer_init(&server->timer); /* Initialized the linked list for delayed callbacks */ #ifndef UA_ENABLE_MULTITHREADING SLIST_INIT(&server->delayedCallbacks); #endif /* Initialized the dispatch queue for worker threads */ #ifdef UA_ENABLE_MULTITHREADING SIMPLEQ_INIT(&server->dispatchQueue); #endif /* Create Namespaces 0 and 1 */ server->namespaces = (UA_String *)UA_Array_new(2, &UA_TYPES[UA_TYPES_STRING]); server->namespaces[0] = UA_STRING_ALLOC(" http://opcfoundation.org/UA/"); UA_String_copy(&server->config.applicationDescription.applicationUri, &server->namespaces[1]); server->namespacesSize = 2; /* Initialized SecureChannel and Session managers */ UA_SecureChannelManager_init(&server->secureChannelManager, server); UA_SessionManager_init(&server->sessionManager, server); /* Add a regular callback for cleanup and maintenance */ UA_Server_addRepeatedCallback(server, (UA_ServerCallback)UA_Server_cleanup, NULL, 10000, NULL); /* Initialized discovery database */ #ifdef UA_ENABLE_DISCOVERY LIST_INIT(&server->registeredServers); server->registeredServersSize = 0; LIST_INIT(&server->periodicServerRegisterCallbacks); server->registerServerCallback = NULL; server->registerServerCallbackData = NULL; #endif /* Initialize multicast discovery */ #if defined(UA_ENABLE_DISCOVERY) && defined(UA_ENABLE_DISCOVERY_MULTICAST) server->mdnsDaemon = NULL; #ifdef _WIN32 server->mdnsSocket = INVALID_SOCKET; #else server->mdnsSocket = -1; #endif server->mdnsMainSrvAdded = UA_FALSE; if(server->config.applicationDescription.applicationType == UA_APPLICATIONTYPE_DISCOVERYSERVER) initMulticastDiscoveryServer(server); LIST_INIT(&server->serverOnNetwork); server->serverOnNetworkSize = 0; server->serverOnNetworkRecordIdCounter = 0; server->serverOnNetworkRecordIdLastReset = UA_DateTime_now(); memset(server->serverOnNetworkHash, 0, sizeof(struct serverOnNetwork_hash_entry*) * SERVER_ON_NETWORK_HASH_PRIME); server->serverOnNetworkCallback = NULL; server->serverOnNetworkCallbackData = NULL; #endif /* Initialize namespace 0*/ UA_StatusCode retVal = UA_Server_initNS0(server); if(retVal != UA_STATUSCODE_GOOD) { UA_LOG_ERROR(config->logger, UA_LOGCATEGORY_SERVER, "Initialization of Namespace 0 failed with %s. " "See previous outputs for any error messages.", UA_StatusCode_name(retVal)); UA_Server_delete(server); return NULL; } return server; } 第四步 给服务器配置数据内存节点 源码片段: server_data = ( SERVER_DATA*)malloc(sizeof( SERVER_DATA)); //结构体SERVER_DATA是我自定义的内部结构体对象,记录服务器数据节点和配配置对象地址,方便后面内部函数的使用// memset(server_data,0,sizeof(SERVER_DATA)); ret_var = Set_Dataspace(opc_server); //函数Set_Dataspace是我自己定义封装的内部实现函数,可以为服务器分配数据节点并映射内存// if(0>ret_var) { UA_Server_delete(opc_server->Server); UA_ServerConfig_delete(opc_server->config); opc_server->Server=NULL; opc_server->config = NULL; return -6; } typedef struct server_data_struct{ UA_NodeId Bool_NodeId,Byte_NodeId,Int_NodeId, Dint_NodeId,Uint_NodeId,Udint_NodeId; OPC_SERVER *opc_server; } SERVER_DATA; //包含服务器节点与数据的对象地址指针// static int Set_Dataspace(OPC_SERVER *opc_server) //配置服务器的内存数据及其访问权限// { if(NULL == opc_server) return -1; if(NULL == server_data) return -1; UA_Server *Server = (opc_server->Server); server_data->opc_server = opc_server; /*配置BOOL变量数据节点到服务器中*/ if(0<(opc_server->bool_length)) { if(NULL==(opc_server->bool_addr)) //服务器内存空间为空,非法!!// return -2; UA_VariableAttributes Bool_attr = UA_VariableAttributes_default; //申明一个数据节点的默认属性对象// UA_Variant_setArrayCopy(&Bool_attr.value,(opc_server->bool_addr),(opc_server->bool_length),&UA_TYPES[UA_TYPES_BOOLEAN]); //将数据内存地址与内存大小定义到数据节点对象中// Bool_attr.description = UA_LOCALIZEDTEXT("en-US","JS BOOL"); //定义数据节点对象的名字// Bool_attr.displayName = UA_LOCALIZEDTEXT("en-US","JS BOOL"); Bool_attr.dataType = UA_TYPES[UA_TYPES_BOOLEAN].typeId; //定义申明数据节点数据的类型// Bool_attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE; //配置数据权限为可读可写// UA_NodeId Bool_NodeId = UA_NODEID_STRING(1,"JS.BOOL"); //创建一个数据节点,一会用来挂载数据并挂载到服务器上, 客户端通过这个节点名:Js.BOOL来访问服务器的这个数据节点里的数据 // UA_QualifiedName Bool_IntegerName = UA_QUALIFIEDNAME(1,"JS BOOL"); UA_NodeId parentNodeId = UA_NODEID_NUMERIC(0,UA_NS0ID_OBJECTSFOLDER); //获取服务器各类数据节点的父节点// UA_NodeId parentReferenceNodeId = UA_NODEID_NUMERIC(0,UA_NS0ID_ORGANIZES); UA_Server_addVariableNode(Server,Bool_NodeId,parentNodeId,\ parentReferenceNodeId,Bool_IntegerName,\ UA_NODEID_NUMERIC(0,UA_NS0ID_BASEDATAVARIABLETYPE),\ Bool_attr,NULL,NULL); //将数据节点连同数据对象一起映射到服务器上// server_data->Bool_NodeId = Bool_NodeId; UA_ValueCallback callback_bool; callback_bool.onRead = WriteData_to_Buffer; //当有客户端请求读取数据节点的数据时,服务器自动执行函数 WriteData_to_Buffer, 这个函数是我内部实现的,函数接口必须遵守open62541的规定 // callback_bool.onWrite = ReadData_from_Buffer; //当有客户端请求写入数据节点的数据时,服务器自动执行函数 ReadData_from_Buffer, 这个函数是我内部实现的,函数接口必须遵守open62541的规定 // UA_Server_setVariableNode_valueCallback(Server,Bool_NodeId,callback_bool); //这里是服务器可以监控客户端的对该数据节点的读写访问, 自动调用指定的函数,这里是为了设置读写状态位 // } /*配置BYTE变量数据节点到服务器中*/ if(0<(opc_server->byte_length)) { if(NULL==(opc_server->byte_addr)) //服务器内存空间为空,非法!!// return -2; UA_VariableAttributes Byte_attr = UA_VariableAttributes_default; UA_Variant_setArrayCopy(&Byte_attr.value,(opc_server->byte_addr),(opc_server->byte_length),&UA_TYPES[UA_TYPES_BYTE]); Byte_attr.description = UA_LOCALIZEDTEXT("en-US","JS BYTE"); Byte_attr.displayName = UA_LOCALIZEDTEXT("en-US","JS BYTE"); Byte_attr.dataType = UA_TYPES[UA_TYPES_BYTE].typeId; Byte_attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE; //配置数据权限为可读可写// UA_NodeId Byte_NodeId = UA_NODEID_STRING(1,"JS.BYTE"); UA_QualifiedName Byte_IntegerName = UA_QUALIFIEDNAME(1,"JS BYTE"); UA_NodeId parentNodeId = UA_NODEID_NUMERIC(0,UA_NS0ID_OBJECTSFOLDER); UA_NodeId parentReferenceNodeId = UA_NODEID_NUMERIC(0,UA_NS0ID_ORGANIZES); UA_Server_addVariableNode(Server,Byte_NodeId,parentNodeId,\ parentReferenceNodeId,Byte_IntegerName,\ UA_NODEID_NUMERIC(0,UA_NS0ID_BASEDATAVARIABLETYPE),\ Byte_attr,NULL,NULL); server_data->Byte_NodeId = Byte_NodeId; /*将节点数据加入自动数据同步中*/ UA_ValueCallback callback_byte; callback_byte.onRead = WriteData_to_Buffer; callback_byte.onWrite = ReadData_from_Buffer; UA_Server_setVariableNode_valueCallback(Server,Byte_NodeId,callback_byte); } /*配置INT变量数据节点到服务器中*/ if(0<(opc_server->int_length)) { if(NULL==(opc_server->int_addr)) //服务器内存空间为空,非法!!// return -2; UA_VariableAttributes Int_attr = UA_VariableAttributes_default; UA_Variant_setArrayCopy(&Int_attr.value,(opc_server->int_addr),(opc_server->int_length),&UA_TYPES[UA_TYPES_INT16]); Int_attr.description = UA_LOCALIZEDTEXT("en-US","JS INT"); Int_attr.displayName = UA_LOCALIZEDTEXT("en-US","JS INT"); Int_attr.dataType = UA_TYPES[UA_TYPES_INT16].typeId; Int_attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE; //配置数据权限为可读可写// UA_NodeId Int_NodeId = UA_NODEID_STRING(1,"JS.INT"); UA_QualifiedName Int_IntegerName = UA_QUALIFIEDNAME(1,"JS INT"); UA_NodeId parentNodeId = UA_NODEID_NUMERIC(0,UA_NS0ID_OBJECTSFOLDER); UA_NodeId parentReferenceNodeId = UA_NODEID_NUMERIC(0,UA_NS0ID_ORGANIZES); UA_Server_addVariableNode(Server,Int_NodeId,parentNodeId,\ parentReferenceNodeId,Int_IntegerName,\ UA_NODEID_NUMERIC(0,UA_NS0ID_BASEDATAVARIABLETYPE),\ Int_attr,NULL,NULL); server_data->Int_NodeId = Int_NodeId; /*将节点数据加入自动数据同步中*/ UA_ValueCallback callback_int; callback_int.onRead = WriteData_to_Buffer; callback_int.onWrite = ReadData_from_Buffer; UA_Server_setVariableNode_valueCallback(Server,Int_NodeId,callback_int); } /*配置DINT变量数据节点到服务器中*/ if(0<(opc_server->dint_length)) { if(NULL==(opc_server->dint_addr)) //服务器内存空间为空,非法!!// return -2; UA_VariableAttributes Dint_attr = UA_VariableAttributes_default; UA_Variant_setArrayCopy(&Dint_attr.value,(opc_server->dint_addr),(opc_server->dint_length),&UA_TYPES[UA_TYPES_INT32]); Dint_attr.description = UA_LOCALIZEDTEXT("en-US","JS DINT"); Dint_attr.displayName = UA_LOCALIZEDTEXT("en-US","JS DINT"); Dint_attr.dataType = UA_TYPES[UA_TYPES_INT32].typeId; Dint_attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE; //配置数据权限为可读可写// UA_NodeId Dint_NodeId = UA_NODEID_STRING(1,"JS.DINT"); UA_QualifiedName Dint_IntegerName = UA_QUALIFIEDNAME(1,"JS DINT"); UA_NodeId parentNodeId = UA_NODEID_NUMERIC(0,UA_NS0ID_OBJECTSFOLDER); UA_NodeId parentReferenceNodeId = UA_NODEID_NUMERIC(0,UA_NS0ID_ORGANIZES); UA_Server_addVariableNode(Server,Dint_NodeId,parentNodeId,\ parentReferenceNodeId,Dint_IntegerName,\ UA_NODEID_NUMERIC(0,UA_NS0ID_BASEDATAVARIABLETYPE),\ Dint_attr,NULL,NULL); server_data->Dint_NodeId = Dint_NodeId; /*将节点数据加入自动数据同步中*/ UA_ValueCallback callback_dint; callback_dint.onRead = WriteData_to_Buffer; callback_dint.onWrite = ReadData_from_Buffer; UA_Server_setVariableNode_valueCallback(Server,Dint_NodeId,callback_dint); } /*配置UINT变量数据节点到服务器中*/ if(0<(opc_server->uint_length)) { if(NULL==(opc_server->uint_addr)) //服务器内存空间为空,非法!!// return -2; UA_VariableAttributes Uint_attr = UA_VariableAttributes_default; UA_Variant_setArrayCopy(&Uint_attr.value,(opc_server->uint_addr),(opc_server->uint_length),&UA_TYPES[UA_TYPES_UINT16]); Uint_attr.description = UA_LOCALIZEDTEXT("en-US","JS UINT"); Uint_attr.displayName = UA_LOCALIZEDTEXT("en-US","JS UINT"); Uint_attr.dataType = UA_TYPES[UA_TYPES_UINT16].typeId; Uint_attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE; //配置数据权限为可读可写// UA_NodeId Uint_NodeId = UA_NODEID_STRING(1,"JS.UINT"); UA_QualifiedName Uint_IntegerName = UA_QUALIFIEDNAME(1,"JS UINT"); UA_NodeId parentNodeId = UA_NODEID_NUMERIC(0,UA_NS0ID_OBJECTSFOLDER); UA_NodeId parentReferenceNodeId = UA_NODEID_NUMERIC(0,UA_NS0ID_ORGANIZES); UA_Server_addVariableNode(Server,Uint_NodeId,parentNodeId,\ parentReferenceNodeId,Uint_IntegerName,\ UA_NODEID_NUMERIC(0,UA_NS0ID_BASEDATAVARIABLETYPE),\ Uint_attr,NULL,NULL); server_data->Uint_NodeId = Uint_NodeId; /*将节点数据加入自动数据同步中*/ UA_ValueCallback callback_uint; callback_uint.onRead = WriteData_to_Buffer; callback_uint.onWrite = ReadData_from_Buffer; UA_Server_setVariableNode_valueCallback(Server,Uint_NodeId,callback_uint); } /*配置DUINT变量数据节点到服务器中*/ if(0<(opc_server->udint_length)) { if(NULL==(opc_server->udint_addr)) //服务器内存空间为空,非法!!// return -2; UA_VariableAttributes Udint_attr = UA_VariableAttributes_default; UA_Variant_setArrayCopy(&Udint_attr.value,(opc_server->udint_addr),(opc_server->udint_length),&UA_TYPES[UA_TYPES_UINT32]); Udint_attr.description = UA_LOCALIZEDTEXT("en-US","JS UDINT"); Udint_attr.displayName = UA_LOCALIZEDTEXT("en-US","JS UDINT"); Udint_attr.dataType = UA_TYPES[UA_TYPES_UINT32].typeId; Udint_attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE; //配置数据权限为可读可写// UA_NodeId Udint_NodeId = UA_NODEID_STRING(1,"JS.UDINT"); UA_QualifiedName Udint_IntegerName = UA_QUALIFIEDNAME(1,"JS UDINT"); UA_NodeId parentNodeId = UA_NODEID_NUMERIC(0,UA_NS0ID_OBJECTSFOLDER); UA_NodeId parentReferenceNodeId = UA_NODEID_NUMERIC(0,UA_NS0ID_ORGANIZES); UA_Server_addVariableNode(Server,Udint_NodeId,parentNodeId,\ parentReferenceNodeId,Udint_IntegerName,\ UA_NODEID_NUMERIC(0,UA_NS0ID_BASEDATAVARIABLETYPE),\ Udint_attr,NULL,NULL); server_data->Udint_NodeId = Udint_NodeId; /*将节点数据加入自动数据同步中*/ UA_ValueCallback callback_udint; callback_udint.onRead = WriteData_to_Buffer; callback_udint.onWrite = ReadData_from_Buffer; UA_Server_setVariableNode_valueCallback(Server,Udint_NodeId,callback_udint); } return 0; } static void WriteData_to_Buffer(UA_Server *server, const UA_NodeId *sessionId, void *sessionContext, const UA_NodeId *nodeid, void *nodeContext, const UA_NumericRange *range, const UA_DataValue *data) { SetBOOL_Clientread(*nodeid); // 调用内部自定义的函数 ,判断被访问的节点数据类型,置位状态位// } static void ReadData_from_Buffer(UA_Server *server, const UA_NodeId *sessionId, void *sessionContext, const UA_NodeId *nodeId, void *nodeContext, const UA_NumericRange *range, const UA_DataValue *data) { SetBOOL_Clientwrite(*nodeId); // 调用内部自定义的函数 ,判断被访问的节点数据类型,置位状态位// } static void SetBOOL_Clientread(UA_NodeId nodeid) //客户请求读取数据节点,对应位置1// { if(NULL == server_data) return; if(NULL == (server_data->opc_server)) return; UA_UInt32 Nodeic = nodeid.identifier.numeric; OPC_SERVER *opc_server = (server_data->opc_server); if(Nodeic == ((server_data->Bool_NodeId).identifier.numeric)) //client read bool type data// opc_server->NodeID_Read[0] = 1; if(Nodeic == ((server_data->Byte_NodeId).identifier.numeric)) //client read byte type data// opc_server->NodeID_Read[1] = 1; if(Nodeic == ((server_data->Int_NodeId).identifier.numeric)) //client read int byte data// opc_server->NodeID_Read[2] = 1; if(Nodeic == ((server_data->Dint_NodeId).identifier.numeric)) //client read dint byte data// opc_server->NodeID_Read[3] = 1; if(Nodeic == ((server_data->Uint_NodeId).identifier.numeric)) //client read uint byte data// opc_server->NodeID_Read[4] = 1; if(Nodeic == ((server_data->Udint_NodeId).identifier.numeric)) //client read udint byte data// opc_server->NodeID_Read[5] = 1; } static void SetBOOL_Clientwrite(UA_NodeId nodeid) //客户端请求写入数据节点,对应位置1// { if(NULL == server_data) return; if(NULL == (server_data->opc_server)) return; UA_UInt32 Nodeic = nodeid.identifier.numeric; OPC_SERVER *opc_server = (server_data->opc_server); if(Nodeic == ((server_data->Bool_NodeId).identifier.numeric)) //client write bool type data// opc_server->NodeID_Write[0] = 1; if(Nodeic == ((server_data->Byte_NodeId).identifier.numeric)) //client write byte type data// opc_server->NodeID_Write[1] = 1; if(Nodeic == ((server_data->Int_NodeId).identifier.numeric)) //client write int byte data// opc_server->NodeID_Write[2] = 1; if(Nodeic == ((server_data->Dint_NodeId).identifier.numeric)) //client write dint byte data// opc_server->NodeID_Write[3] = 1; if(Nodeic == ((server_data->Uint_NodeId).identifier.numeric)) //client write uint byte data// opc_server->NodeID_Write[4] = 1; if(Nodeic == ((server_data->Udint_NodeId).identifier.numeric)) //client write udint byte data// opc_server->NodeID_Write[5] = 1; } 解析: 1、 结构体 UA_VariableAttributes 是open62541定义的用来描述一个服务器数据的结构体对象, 服务器上挂载数据节点时,每个有效的数据节点都要包含这个对象: typedef struct { UA_UInt32 specifiedAttributes; UA_LocalizedText displayName; //描述数据对象的名字// UA_LocalizedText description; //数据对象的说明// UA_UInt32 writeMask; UA_UInt32 userWriteMask; UA_Variant value; //这里映射描述数据对象具体的内存空间地址// UA_NodeId dataType; //描述数据内存的数据类型// UA_Int32 valueRank; size_t arrayDimensionsSize; UA_UInt32 *arrayDimensions; UA_Byte accessLevel; //数据开放的可操作权限配置// UA_Byte userAccessLevel; UA_Double minimumSamplingInterval; UA_Boolean historizing; } UA_VariableAttributes; 2、 结构体 UA_Variant 是open62541定义的用来描述具体的数据空间的结构体,这个结构体在后面数据同步中扮演着十分重要的角色, 内存通过服务器与客户端进行数据的同步需要通过这个结构体对象来实现: typedef struct { const UA_DataType *type; /* The data type description */ UA_VariantStorageType storageType; size_t arrayLength; /* The number of elements in the data array */ void *data; /* Points to the scalar or array data */ size_t arrayDimensionsSize; /* The number of dimensions */ UA_UInt32 *arrayDimensions; /* The length of each dimension */ } UA_Variant; 3、 结构体 UA_NodeId 是open62451定义的用来描述服务器数据挂载节点的结构体,结构体内部用过联合体数据来区分不同的数据节点对象,在创建数据节点时,由open62541内部自动分配: typedef struct { UA_UInt16 namespaceIndex; //open ua支持的数据节点可挂载的数据类型很多,普通的数据固定使用Index为1// enum UA_NodeIdType identifierType; //枚举量指明节点采用联合体的何种数据类型标签来分类。// union { UA_UInt32 numeric; UA_String string; UA_Guid guid; UA_ByteString byteString; } identifier; //用来对节点数据进行分类管理的联合体// } UA_NodeId; enum UA_NodeIdType { UA_NODEIDTYPE_NUMERIC = 0, /* In the binary encoding, this can also become 1 or 2 (2byte and 4byte encoding of small numeric nodeids) */ UA_NODEIDTYPE_STRING = 3, UA_NODEIDTYPE_GUID = 4, UA_NODEIDTYPE_BYTESTRING = 5 }; 4、 结构体 UA_QualifiedName 是open62541定义的描述数据节点挂在载服务器上的签名,每个不同的挂载在同个服务器上的数据节点,其签名必须不同: typedef struct { UA_UInt16 namespaceIndex; UA_String name; } UA_QualifiedName; 5、 函数接口 static UA_INLINE UA_NodeId UA_NODEID_STRING(UA_UInt16 nsIndex, char *chars) 是open62541提供的配置生成UA_NodeId 对象的函数, 对象标识采用枚举中的string: static UA_INLINE UA_NodeId UA_NODEID_STRING(UA_UInt16 nsIndex, char *chars) { UA_NodeId id; id.namespaceIndex = nsIndex; id.identifierType = UA_NODEIDTYPE_STRING; id.identifier.string = UA_STRING(chars); return id; } 6、 函数接口 static UA_INLINE UA_NodeId UA_NODEID_NUMERIC(UA_UInt16 nsIndex, UA_UInt32 identifier) 与上面的函数类似的功能,只不过使用的标识枚举量是numric: static UA_INLINE UA_NodeId UA_NODEID_NUMERIC(UA_UInt16 nsIndex, UA_UInt32 identifier) { UA_NodeId id; id.namespaceIndex = nsIndex; id.identifierType = UA_NODEIDTYPE_NUMERIC; id.identifier.numeric = identifier; return id; } 7、 结构体 UA_ValueCallback 是open62541定义的为了支持对客户请求状态的检查,并提供实现自动执行动作成为可能的一个结构体, 它用来定义当客户端发送读写数据请求时,调用的函数对象: typedef struct { /* Called before the value attribute is read. It is possible to write into the * value attribute during onRead (using the write service). The node is * re-opened afterwards so that changes are considered in the following read * operation. * * @param handle Points to user-provided data for the callback. * @param nodeid The identifier of the node. * @param data Points to the current node value. * @param range Points to the numeric range the client wants to read from * (or NULL). */ void (*onRead)(UA_Server *server, const UA_NodeId *sessionId, void *sessionContext, const UA_NodeId *nodeid, void *nodeContext, const UA_NumericRange *range, const UA_DataValue *value); /* Called after writing the value attribute. The node is re-opened after * writing so that the new value is visible in the callback. * * @param server The server executing the callback * @sessionId The identifier of the session * @sessionContext Additional data attached to the session * in the access control layer * @param nodeid The identifier of the node. * @param nodeUserContext Additional data attached to the node by * the user. * @param nodeConstructorContext Additional data attached to the node * by the type constructor(s). * @param range Points to the numeric range the client wants to write to (or * NULL). */ void (*onWrite)(UA_Server *server, const UA_NodeId *sessionId, void *sessionContext, const UA_NodeId *nodeId, void *nodeContext, const UA_NumericRange *range, const UA_DataValue *data); } UA_ValueCallback; 8、 函数 UA_StatusCode UA_Server_setVariableNode_valueCallback(UA_Server *server, const UA_NodeId nodeId, const UA_ValueCallback callback) 是open62541提供的用于支持客户端访问服务器数据自动监控功能的函数实现, 可以通过他实现服务器的数据自动同步: UA_StatusCode UA_Server_setVariableNode_valueCallback(UA_Server *server, const UA_NodeId nodeId, const UA_ValueCallback callback) { return UA_Server_editNode (server, &server->adminSession, &nodeId, (UA_EditNodeCallback)setValueCallback, /* cast away const because callback uses const anyway */ (UA_ValueCallback *)(uintptr_t) &callback); } UA_StatusCode UA_Server_editNode (UA_Server *server, UA_Session *session, const UA_NodeId *nodeId, UA_EditNodeCallback callback, void *data) { #ifndef UA_ENABLE_MULTITHREADING const UA_Node *node = UA_Nodestore_get(server, nodeId); if(!node) return UA_STATUSCODE_BADNODEIDUNKNOWN; UA_StatusCode retval = callback(server, session, (UA_Node*)(uintptr_t)node, data); UA_Nodestore_release(server, node); return retval; #else UA_StatusCode retval; do { UA_Node *node; retval = server->config.nodestore.getNodeCopy(server->config.nodestore.context, nodeId, &node); if(retval != UA_STATUSCODE_GOOD) return retval; retval = callback(server, session, node, data); if(retval != UA_STATUSCODE_GOOD) { server->config.nodestore.deleteNode(server->config.nodestore.context, node); return retval; } retval = server->config.nodestore.replaceNode(server->config.nodestore.context, node); } while(retval != UA_STATUSCODE_GOOD); return retval; #endif } 3、启动和停止已经被创建的服务器: int Run_Server(OPC_SERVER *opc_server) 、 int Stop_Server(OPC_SERVER *opc_server); 函数接口也是对open62541提供的功能函数进行的二次封装,独立线程启动服务器并监控客户端的读写,在停止服务器后,并不会立即释放服务器相关资源,可以反复启动和停止。 源码片段: int Run_Server(OPC_SERVER *opc_server) //启动服务器// { if(NULL == opc_server) return -1; if(NULL == (opc_server->Server)) return -2; opc_server->Enable=1; //这里会自动置1 Enable// pthread_t p_id; pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED); pthread_create(&p_id,&attr , Pthread_Server_functions,(void*)opc_server); //独立线程,阻塞监听客户端链接与断开// return 0; } int Stop_Server(OPC_SERVER *opc_server) //停止服务器// { if(NULL == opc_server) return -1; if(NULL == (opc_server->Server)) return -2; opc_server->Enable=0; //设置Enable =0 ,被阻塞的线程会停止服务器器运行,并释放// return 0; } static void *Pthread_Server_functions(void *arg) //独立的监控线程// { if(NULL == arg) return NULL; OPC_SERVER *opc_server = (OPC_SERVER*)arg; UA_StatusCode retval = UA_Server_run((opc_server->Server),&(opc_server->Enable)); / /这里调用了open62541提供的函数接口,该函数是阻塞执行的当Enable=1时会阻塞该线程直到Enable=0// if(retval != UA_STATUSCODE_GOOD) printf("The sever start is fail\n"); return NULL; } 解析: 1、 函数 UA_StatusCode UA_Server_run(UA_Server *server, volatile UA_Boolean *running) 是open62541实现的用于阻塞启动服务器和关闭服务器的开关类函数接口: UA_StatusCode UA_Server_run(UA_Server *server, volatile UA_Boolean *running) { UA_StatusCode retval = UA_Server_run_startup(server); //这里执行记录服务器启动的开始时间,并开始建立网络socket链接// if(retval != UA_STATUSCODE_GOOD) return retval; #ifdef UA_ENABLE_VALGRIND_INTERACTIVE size_t loopCount = 0; #endif while(*running) { #ifdef UA_ENABLE_VALGRIND_INTERACTIVE if(loopCount == 0) { VALGRIND_DO_LEAK_CHECK; } ++loopCount; loopCount %= UA_VALGRIND_INTERACTIVE_INTERVAL; #endif UA_Server_run_iterate(server, true); //内部循环定时监控网络端口,处理客户端的链接与断开// } return UA_Server_run_shutdown(server); //对服务器作关闭的相关处理工作,这里的处理非阻塞, 函数返回并不代表服务器已经关闭,所以在关闭服务器后,不要立即释放相关资源!! // } 4、调用函数,设备通过服务器与远程客户端同步数据: Updata_to_server 、 Getdata_from_server 实际上,关于内存与opc ua服务器之间的数据同步,这里我采用的手动的方式,即开放出函数接口,自由调用; 数据从内存写入到服务器上的实现,我采用了先读后改再写入的方式,避免数据写入对其他数据的影响和覆盖。 内存同步到服务器 源码片段: int Updata_to_server(OPC_SERVER *opc_server,const UA_UInt32 Data_type,const UA_UInt32 Data_num,const void *Data_value) { if(NULL == opc_server) return -1; if(NULL == (opc_server->Server)) return -3; if(NULL == Data_value) return -2; /*若需要更新服务器上的数据,必须先将服务器上的数据读出来,再更改后写回去*/ switch(Data_type) { case 0: //BOOL TYPE// { GetData_from_NodeID((opc_server->Server),(server_data->Bool_NodeId)); //先读取服务器数据到内存// opc_server->bool_addr[Data_num] = *((UA_Boolean*)Data_value); SetData_to_NodeID((opc_server->Server),(server_data->Bool_NodeId)); //这里调用的是我自己定义的内部实现函数,是对整个数据节点的数据的操作// opc_server->NodeID_Read[0] = 0; opc_server->NodeID_Write[0] = 0; //服务器自己写内存也会触发状态置位// break; } case 1: //BYTE TYPE// { GetData_from_NodeID((opc_server->Server),(server_data->Byte_NodeId)); opc_server->byte_addr[Data_num] = *((UA_Byte*)Data_value); SetData_to_NodeID((opc_server->Server),(server_data->Byte_NodeId)); opc_server->NodeID_Read[1] = 0; opc_server->NodeID_Write[1] = 0; //服务器自己写内存也会触发状态置位// break; } case 2: //INT TYPE// { GetData_from_NodeID((opc_server->Server),(server_data->Int_NodeId)); opc_server->int_addr[Data_num] = *((UA_Int16*)Data_value); SetData_to_NodeID((opc_server->Server),(server_data->Int_NodeId)); opc_server->NodeID_Read[2] = 0; opc_server->NodeID_Write[2] = 0; //服务器自己写内存也会触发状态置位// break; } case 3: //DINT TYPE// { GetData_from_NodeID((opc_server->Server),(server_data->Dint_NodeId)); opc_server->dint_addr[Data_num] = *((UA_Int32*)Data_value); SetData_to_NodeID((opc_server->Server),(server_data->Dint_NodeId)); opc_server->NodeID_Read[3] = 0; opc_server->NodeID_Write[3] = 0; //服务器自己写内存也会触发状态置位// break; } case 4: //UINT TYPE// { GetData_from_NodeID((opc_server->Server),(server_data->Uint_NodeId)); opc_server->uint_addr[Data_num] = *((UA_UInt16*)Data_value); SetData_to_NodeID((opc_server->Server),(server_data->Uint_NodeId)); opc_server->NodeID_Read[4] = 0; opc_server->NodeID_Write[4] = 0; //服务器自己写内存也会触发状态置位// break; } case 5: //UDINT TYPE// { GetData_from_NodeID((opc_server->Server),(server_data->Udint_NodeId)); opc_server->udint_addr[Data_num] = *((UA_UInt32*)Data_value); SetData_to_NodeID((opc_server->Server),(server_data->Udint_NodeId)); opc_server->NodeID_Read[5] = 0; opc_server->NodeID_Write[5] = 0; //服务器自己写内存也会触发状态置位// break; } } return 0; } static void SetData_to_NodeID(UA_Server *server,UA_NodeId nodeid) { if(NULL == server_data) return; OPC_SERVER *opc_server = (server_data->opc_server); UA_Variant Value; //通过这个open62541提供的数据描述结构体来传递内存数据到服务器// UA_Variant_init(&Value); UA_UInt32 Nodeic = nodeid.identifier.numeric; if(Nodeic == ((server_data->Bool_NodeId).identifier.numeric)) //将对BOOL变量数据节点进行写入同步// { Value.type = &UA_TYPES[UA_TYPES_BOOLEAN]; Value.arrayLength = (opc_server->bool_length); Value.data = (void*)(opc_server->bool_addr); } else if(Nodeic == ((server_data->Byte_NodeId).identifier.numeric)) //将对BYTE变量数据节点进行写入同步// { Value.type = &UA_TYPES[UA_TYPES_BYTE]; Value.arrayLength = (opc_server->byte_length); Value.data = (void*)(opc_server->byte_addr); } else if(Nodeic == ((server_data->Int_NodeId).identifier.numeric)) //将对INT变量数据节点进行写入同步// { Value.type = &UA_TYPES[UA_TYPES_INT16]; Value.arrayLength = (opc_server->int_length); Value.data = (void*)(opc_server->int_addr); } else if(Nodeic == ((server_data->Dint_NodeId).identifier.numeric)) //将对DINT变量数据节点进行写入同步// { Value.type = &UA_TYPES[UA_TYPES_INT32]; Value.arrayLength = (opc_server->dint_length); Value.data = (void*)(opc_server->dint_addr); } else if(Nodeic == ((server_data->Uint_NodeId).identifier.numeric)) //将对UINT变量数据节点进行写入同步// { Value.type = &UA_TYPES[UA_TYPES_UINT16]; Value.arrayLength = (opc_server->uint_length); Value.data = (void*)(opc_server->uint_addr); } else if(Nodeic == ((server_data->Udint_NodeId).identifier.numeric)) //将对UDINT变量数据节点进行写入同步// { Value.type = &UA_TYPES[UA_TYPES_UINT32]; Value.arrayLength = (opc_server->udint_length); Value.data = (void*)(opc_server->udint_addr); } UA_Server_writeValue(server,nodeid,Value); //这是open62541定义的内存同步到服务器的函数接口// } 解析: 1、 函数 static UA_INLINE UA_StatusCode UA_Server_writeValue(UA_Server *server, const UA_NodeId nodeId, const UA_Variant value) 是open62541实现的数据同步函数接口, 函数通过结构体 UA_Variant 以统一的方式将各种不同类型的数据统一同步给服务器内存: static UA_INLINE UA_StatusCode UA_Server_writeValue(UA_Server *server, const UA_NodeId nodeId, const UA_Variant value) { return __UA_Server_write (server, &nodeId, UA_ATTRIBUTEID_VALUE, &UA_TYPES[UA_TYPES_VARIANT], &value); } UA_StatusCode __UA_Server_write(UA_Server *server, const UA_NodeId *nodeId, const UA_AttributeId attributeId, const UA_DataType *attr_type, const void *attr) { UA_WriteValue wvalue; UA_WriteValue_init(&wvalue); wvalue.nodeId = *nodeId; wvalue.attributeId = attributeId; wvalue.value.hasValue = true; if(attr_type != &UA_TYPES[UA_TYPES_VARIANT]) { /* hacked cast. the target WriteValue is used as const anyway */ UA_Variant_setScalar(&wvalue.value.value, (void*)(uintptr_t)attr, attr_type); } else { wvalue.value.value = *(const UA_Variant*)attr; } return UA_Server_write(server, &wvalue); } 读取服务器数据到内存: 源码片段: int Getdata_from_server(OPC_SERVER *opc_server,const UA_UInt32 Data_type,const UA_UInt32 Data_num,void *Data_value) { if(NULL == opc_server) return -1; if(NULL == (opc_server->Server)) return -3; if(NULL == Data_value) return -2; switch(Data_type) { case 0: //BOOL TYPE// { GetData_from_NodeID((opc_server->Server),(server_data->Bool_NodeId)); //这里是我自定义的内部函数,读取指定节点的数据到内存中// *((UA_Boolean*)Data_value) = (opc_server->bool_addr[Data_num]); opc_server->NodeID_Write[0] = 0; opc_server->NodeID_Read[0] = 0; break; } case 1: //BYTE TYPE// { GetData_from_NodeID((opc_server->Server),(server_data->Byte_NodeId)); *((UA_Byte*)Data_value) = (opc_server->byte_addr[Data_num]); opc_server->NodeID_Write[1] = 0; opc_server->NodeID_Read[1] = 0; break; } case 2: //INT TYPE// { GetData_from_NodeID((opc_server->Server),(server_data->Int_NodeId)); *((UA_Int16*)Data_value) = (opc_server->int_addr[Data_num]); opc_server->NodeID_Write[2] = 0; opc_server->NodeID_Read[2] = 0; break; } case 3: //DINT TYPE// { GetData_from_NodeID((opc_server->Server),(server_data->Dint_NodeId)); *((UA_Int32*)Data_value) = (opc_server->dint_addr[Data_num]); opc_server->NodeID_Write[3] = 0; opc_server->NodeID_Read[3] = 0; break; } case 4: //UINT TYPE// { GetData_from_NodeID((opc_server->Server),(server_data->Uint_NodeId)); *((UA_UInt16*)Data_value) = (opc_server->uint_addr[Data_num]); opc_server->NodeID_Write[4] = 0; opc_server->NodeID_Read[4] = 0; break; } case 5: //UDINT TYPE// { GetData_from_NodeID((opc_server->Server),(server_data->Udint_NodeId)); *((UA_UInt32*)Data_value) = (opc_server->udint_addr[Data_num]); opc_server->NodeID_Write[5] = 0; opc_server->NodeID_Read[5] = 0; break; } } return 0; } static void GetData_from_NodeID(UA_Server *server,UA_NodeId nodeid) { if(NULL == server_data) return; OPC_SERVER *opc_server = (server_data->opc_server); UA_Variant Value; //通过open62541提供的统一的数据结构体来将服务器数据读取出来// UA_Variant_init(& Value); UA_Server_readValue(server,nodeid,&Value); //这里调用open62541提供的函数接口,从服务器读取指定数据节点的数据到 Value// UA_UInt32 Nodeic = nodeid.identifier.numeric; if(Nodeic == ((server_data->Bool_NodeId).identifier.numeric)) //将对BOOL变量数据节点进行读取到内存// memcpy((opc_server->bool_addr), Value.data,sizeof(UA_Boolean)*(opc_server->bool_length)); else if(Nodeic == ((server_data->Byte_NodeId).identifier.numeric)) //将对BYTE变量数据节点进行读取到内存// memcpy((opc_server->byte_addr), Value.data,sizeof(UA_Byte)*(opc_server->byte_length)); else if(Nodeic == ((server_data->Int_NodeId).identifier.numeric)) //将对INT变量数据节点进行读取到内存// memcpy((opc_server->int_addr), Value.data,sizeof(UA_Int16)*(opc_server->int_length)); else if(Nodeic == ((server_data->Dint_NodeId).identifier.numeric)) //将对DINT变量数据节点进行读取到内存// memcpy((opc_server->dint_addr), Value.data,sizeof(UA_Int32)*(opc_server->dint_length)); else if(Nodeic == ((server_data->Uint_NodeId).identifier.numeric)) //将对UINT变量数据节点进行读取到内存// memcpy((opc_server->uint_addr), Value.data,sizeof(UA_UInt16)*(opc_server->uint_length)); else if(Nodeic == ((server_data->Udint_NodeId).identifier.numeric)) //将对UDINT变量数据节点进行读取到内存// memcpy((opc_server->udint_addr), Value.data,sizeof(UA_UInt32)*(opc_server->udint_length)); } 解析: 1、 函数 static UA_INLINE UA_StatusCode UA_Server_readValue(UA_Server *server, const UA_NodeId nodeId, UA_Variant *outValue) 是open62541提供的内部函数接口,支持将不同数据类型节点的数据读取后,以统一的结构体返回: static UA_INLINE UA_StatusCode UA_Server_readValue(UA_Server *server, const UA_NodeId nodeId, UA_Variant *outValue) { return __UA_Server_read (server, &nodeId, UA_ATTRIBUTEID_VALUE, outValue); } UA_StatusCode __UA_Server_read(UA_Server *server, const UA_NodeId *nodeId, const UA_AttributeId attributeId, void *v) { /* Call the read service */ UA_ReadValueId item; UA_ReadValueId_init(&item); item.nodeId = *nodeId; item.attributeId = attributeId; UA_DataValue dv = UA_Server_read(server, &item, UA_TIMESTAMPSTORETURN_NEITHER); /* Check the return value */ UA_StatusCode retval = UA_STATUSCODE_GOOD; if(dv.hasStatus) retval = dv.status; else if(!dv.hasValue) retval = UA_STATUSCODE_BADUNEXPECTEDERROR; if(retval != UA_STATUSCODE_GOOD) { UA_DataValue_deleteMembers(&dv); return retval; } if(attributeId == UA_ATTRIBUTEID_VALUE || attributeId == UA_ATTRIBUTEID_ARRAYDIMENSIONS) { /* Return the entire variant */ memcpy(v, &dv.value, sizeof(UA_Variant)); } else { /* Return the variant content only */ memcpy(v, dv.value.data, dv.value.type->memSize); UA_free(dv.value.data); } return retval; } 5、释放掉服务器的所有资源: void Free_opcua_server(OPC_SERVER *opc_server) 代码片段: void Free_opcua_server(OPC_SERVER *opc_server) { if(NULL == opc_server) return; UA_Server_delete(opc_server->Server); //open62451提供的内部函数,释放已经成功创建的服务器对象// UA_ServerConfig_delete(opc_server->config); //open62541提供的函数接口,释放服务器的配置结构体// free(opc_server); if(NULL != server_data) free(server_data); server_data=NULL; } 解析:调用这些函数请,请确保服务器已经关闭,且关闭后延时一段时间再释放内存。 /// 附加说明:本人承接Linux系统的嵌入式软件开发项目,CODESYS的runntime组件开发。欢迎加微:wxk101633(备注:委托开发)