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

VS2019c++环境下OPCUA+Kepserver+open62541实现与三菱plc通信

文章目录

  • 前言
  • KepServer
  • Open62541
      • 1️⃣ 环境准备(Linux / Windows 通用)
      • 2️⃣ 最小 OPC UA 服务器
      • 3️⃣ 最小 OPC UA 客户端
      • 4️⃣ 一个订阅同时监控多个节点
      • 5️⃣ 自定义数据类型(结构体)
      • 📚 继续阅读
  • 实际封装使用
    • 具体代码梳理如下:


前言

KepServerEx是一款在工业控制中比较常见的数据采集服务软件之一,提供了多种类型的驱动,具有比较广泛的适用性。很多厂商和个人都会选择用它来做OPCServer。在项目的实施或测试过程中,我们有时会遇到身边没有传感器、PLC之类设备的情况,就无法通过实时数据来测试工作成果的有效性。幸运的是KepServerEx提供了数据模拟功能,可以提供多种类型、格式的模拟数据。下面就是具体的配置过程,重点在于模拟数据的函数( 斜坡函数、随机函数、正弦函数、User 函数)
总的来说KEPServer是一款非常强大的数据采集软件,内置了很多设备驱动,基于简单的配置即可采集到各种设备数据,简直是工控行业的神器。


KepServer

kepserver是opcua服务器,上位机和plc是客户端

首先是kepserver的主界面
在这里插入图片描述
系统自带了仿真器驱动 Simulator,可以创建16位地址设备和8位地址设备,方便自己仿真测试

打开工具栏的QC(quick client)图标,可以查看设备的实时数据
在这里插入图片描述
对一个item在一个client上同步写入数值
在这里插入图片描述
在server上新建一个item
在这里插入图片描述

添加通道
在这里插入图片描述
可选项非常的多,无论是Modbus还是OPC都可以选择
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
按照需要跟着添加向导设置自己需要的通道即可
在这里插入图片描述

2、打开安装目录下的server_admin.exe,打开后会在windows任务栏右下角有个图标,右键选择“OPCUA配置”

(上面说的加用户名和密码是这里的“设置”里面)

(下文将要说的重新初始化也在这个图片里)
在这里插入图片描述
打开后窗口如下
在这里插入图片描述
(1)添加新的opcua配置

(2)选择合适的网卡,合适的IP才能被访问到。

(3)配置出来的opcua endpoint,包含IP和端口,别人也是通过这个来访问opcua数据源

(4)设置安全策略,自己测试的话建议只勾选“无”

(5)确定

配置完成后需要重新初始化一下。在上上图里,任务栏软件右键菜单里有个“重新初始化”选项。

最终这个 “opc.tcp://172.23.12.26:49320”就是opcua client的连接参数。

Open62541

open62541 是一个开源的、可嵌入的 OPC UA(OPC Unified Architecture)实现库,采用纯 C 语言(C99)编写。它提供完整的 OPC UA 客户端和服务器功能,专为工业自动化、物联网(IoT)和嵌入式系统设计。

📚 继续阅读
• 官方文档(HTML/PDF):docs/open62541.pdf
• 官方示例:examples/ 里有 50+ 完整 demo
• 中文深入指南:《OPC统一架构 open62541 指南》

下面给出一份 面向初学者的 open62541(v1.3+)使用教程,涵盖

  • 环境准备 → 最小服务器 → 最小客户端 → 多节点订阅 → 自定义数据类型 → XML 信息模型导入
    六大主题,全部给出可直接编译运行的 C 代码片段与 CMake 指令。

1️⃣ 环境准备(Linux / Windows 通用)

git clone https://github.com/open62541/open62541.git
cd open62541 && mkdir build && cd build
cmake .. \-DBUILD_SHARED_LIBS=ON \-DUA_ENABLE_SUBSCRIPTIONS=ON \-DUA_ENABLE_ENCRYPTION=OPENSSL \-DUA_ENABLE_HISTORIZING=ON
make -j && sudo make install

若只需单文件版,可 cmake -DUA_ENABLE_AMALGAMATION=ON ..,会生成 open62541.h + open62541.c,直接拖进工程即可。


2️⃣ 最小 OPC UA 服务器

server_minimal.c

#include <open62541/server.h>
#include <open62541/server_config_default.h>int main(void) {UA_Server *server = UA_Server_new();UA_ServerConfig_setDefault(UA_Server_getConfig(server));/* 添加一个变量节点 “the.answer” */UA_UInt32 ns = UA_Server_addNamespace(server, "http://example.org");UA_VariableAttributes attr = UA_VariableAttributes_default;UA_Int32 myVar = 42;UA_Variant_setScalarCopy(&attr.value, &myVar, &UA_TYPES[UA_TYPES_INT32]);attr.displayName = UA_LOCALIZEDTEXT("en-US","the answer");UA_Server_addVariableNode(server,UA_NODEID_NUMERIC(ns, 1001),UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),UA_QUALIFIEDNAME(ns, "the answer"),UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE),attr, NULL, NULL);UA_StatusCode retval = UA_Server_runUntilInterrupt(server);UA_Server_delete(server);return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}

编译 & 运行

gcc server_minimal.c -o server_minimal -lopen62541
./server_minimal

UaExpert 连接 opc.tcp://localhost:4840 能看到变量 the answer = 42


3️⃣ 最小 OPC UA 客户端

client_minimal.c

#include <open62541/client.h>
#include <open62541/client_config_default.h>int main(void) {UA_Client *client = UA_Client_new();UA_ClientConfig_setDefault(UA_Client_getConfig(client));UA_StatusCode retval = UA_Client_connect(client, "opc.tcp://localhost:4840");if(retval != UA_STATUSCODE_GOOD) goto cleanup;UA_Variant value;UA_Variant_init(&value);retval = UA_Client_readValueAttribute(client,UA_NODEID_STRING(1, "the answer"), &value);if(retval == UA_STATUSCODE_GOOD && UA_Variant_hasScalarType(&value, &UA_TYPES[UA_TYPES_INT32]))printf("Value=%d\n", *(UA_Int32*)value.data);cleanup:UA_Variant_clear(&value);UA_Client_delete(client);return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}

编译 & 测试

gcc client_minimal.c -o client_minimal -lopen62541
./client_minimal

4️⃣ 一个订阅同时监控多个节点

官方 FAQ 答案:一个订阅(Subscription)可以挂任意数量的 MonitoredItem,代码如下:

client_sub_multi.c

#include <open62541/client.h>
#include <open62541/client_subscriptions.h>static void handler_DataChanged(UA_Client *client, UA_UInt32 subId,void *subContext, UA_UInt32 monId, void *monContext, UA_DataValue *value) {UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,"MonitoredItem %u | Value=%d", monId, *(UA_Int32*)value->value.data);
}int main(void) {UA_Client *client = UA_Client_new();UA_ClientConfig_setDefault(UA_Client_getConfig(client));UA_Client_connect(client, "opc.tcp://localhost:4840");UA_UInt32 subId;UA_Client_Subscriptions_create(client,UA_SubscriptionSettings_default, NULL, NULL, &subId);/* 监控两个节点 */UA_NodeId ids[2] = {UA_NODEID_NUMERIC(1, 1001),            /* the.answer */UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERSTATUS_CURRENTTIME)};for(int i=0;i<2;i++){UA_Client_MonitoredItems_createDataChange(client, subId,UA_TIMESTAMPSTORETURN_BOTH, ids[i], UA_ATTRIBUTEID_VALUE,&handler_DataChanged, NULL, NULL);}while(true) UA_Client_run_iterate(client, 100);return 0;
}

5️⃣ 自定义数据类型(结构体)

定义 → 注册 → 使用节点,三步走:

mytype.h

typedef struct {UA_Float temperature;UA_Float humidity;
} SensorData;/* 让 open62541 能序列化/反序列化 SensorData */
static UA_DataTypeMember SensorData_members[2] = {{ UA_TYPES_FLOAT, 0, false, false, offsetof(SensorData, temperature), "Temperature" },{ UA_TYPES_FLOAT, 0, false, false, offsetof(SensorData, humidity),    "Humidity"    }
};
static UA_DataType SensorDataType = {UA_TYPENAME("SensorData") UA_NODEID_NUMERIC(1, 3001),sizeof(SensorData), 0, 2, SensorData_members, UA_NODEID_NULL
};

注册并添加变量:

UA_Server_addDataTypeNode(server, UA_NODEID_NUMERIC(1, 3001),UA_NODEID_NUMERIC(0, UA_NS0ID_STRUCTURE),UA_NODEID_NUMERIC(0, UA_NS0ID_HASSUBTYPE),UA_QUALIFIEDNAME(1, "SensorData"), UA_NODEID_NULL, NULL);SensorData s = { 23.5f, 60.0f };
UA_VariableAttributes vAttr = UA_VariableAttributes_default;
UA_Variant_setScalar(&vAttr.value, &s, &SensorDataType);
UA_Server_addVariableNode(server, UA_NODEID_NUMERIC(1, 3002), ...);

📚 继续阅读

• 官方文档(HTML/PDF):docs/open62541.pdf
• 官方示例:examples/ 里有 50+ 完整 demo
• 中文深入指南:《OPC统一架构 open62541 指南》

至此,你已能完成 open62541 的环境搭建、最小服务器/客户端、多节点订阅、自定义类型及 XML 模型导入。把示例跑通后,再按需深入加密、PubSub、历史数据等高级主题即可。

实际封装使用

Kepserver中OPCUA地址端口设置
在这里插入图片描述
使用Advanced Tags把simulator信号与PLC信号连接起来
在这里插入图片描述
使用Link Tag Type功能实现数据转发
具体的设置,设置好pc模拟信号转发plc与plc转发给pc的模拟信号
在这里插入图片描述
然后使用open62541连接上OPCUA客户端,再来读取kepserver中设置好的pc模拟信号

具体代码梳理如下:

opctool.h

class OpcTool
{
public:OpcTool();~OpcTool();public://心跳信号static int heartsignal;//拍照信号static int camsignal;static int camsignals[4];// 心跳接收static UA_NodeId recvHeart;// 控制信号(接收)// 拍照static UA_NodeId camGarb;public://连接OPC服务器int ConnectOPC(UA_Client*& opcclient, const char* ip);//添加订阅void addMonitoredItemToVariable(UA_Client* client, UA_NodeId* target, UA_Client_DataChangeNotificationCallback callback);void addConnectMonitoredItemToVariable(UA_Client* client);void addControlMonitoredItemToVariable(UA_Client* client);//控制信号订阅static void handler_ControlDataChanged(UA_Client* client, UA_UInt32 subID,void* subContext, UA_UInt32 monID,void* monContext, UA_DataValue* value);//连接信号订阅static void handler_ConnectDataChanged(UA_Client* client, UA_UInt32 subID,void* subContext, UA_UInt32 monID,void* monContext, UA_DataValue* value);//心跳接收信号订阅static void handler_PLCHeartChanged(UA_Client* client, UA_UInt32 subID,void* subContext, UA_UInt32 monID,void* monContext, UA_DataValue* value);//断开与OPC服务器的连接void DisConnectOPC(UA_Client*& opcclient);//初始化信号void InitSignal(UA_Client*& opcclient);//清空信号void ClearSignal_Camera1(UA_Client*& opcclient);//发送bool信号void SendBoolSignal(UA_Client*& opcclient, UA_NodeId* target, bool flag);//发送short信号void SendShortSignal(UA_Client*& opcclient, UA_NodeId* target, short num);//发送float信号void SendFloatSignal(UA_Client*& opcclient, UA_NodeId* target, float value);//发送string信号void SendStringSignal(UA_Client*& opcclient, UA_NodeId* target, std::string value);
private:void delayTime(int msec);DWORD CreateDWORD(int a3, int a2, int a1, int a0);BYTE statesignal = 0;int resultflag;int camerastate;};

UA_NodeId与kepserver信号绑定

// 状态信号(接收)// 心跳接收(short)
UA_NodeId OpcTool::recvHeart = UA_NODEID_STRING(2, (char*)"connect.signal.心跳接收");// 拍照(bool)
UA_NodeId OpcTool::camGarb = UA_NODEID_STRING(2, (char*)"connect.signal.涂胶检测状态");

实例化

OpcTool opcTool;
UA_Client* opcclient = UA_Client_new();
UA_Client* opcclient_cam = UA_Client_new();
opcTool.addControlMonitoredItemToVariable(opcclient_cam);

UA_Client连接OPCUA服务器

int OpcTool::ConnectOPC(UA_Client*& opcclient, const char* ip)
{// 连接客户端(UA_Client 客户端类;ip 服务器ip)//UA_Client* client = UA_Client_new();UA_ClientConfig_setDefault(UA_Client_getConfig(opcclient));UA_StatusCode status = UA_Client_connect(opcclient, (char*)ip);if (status != UA_STATUSCODE_GOOD){UA_Client_delete(opcclient);return -1;}else{//opcclient = client;return 0;}
}

连接初始化

void VisionROIDialog::LoadConnect()
{int Res = opcTool.ConnectOPC(opcclient, "opc.tcp://127.0.0.1:49320");Res += opcTool.ConnectOPC(opcclient_cam, "opc.tcp://127.0.0.1:49320");Res += opcTool.ConnectOPC(opcclient_sendHeart, "opc.tcp://127.0.0.1:49320");Res += opcTool.ConnectOPC(opcclient_recvHeart, "opc.tcp://127.0.0.1:49320");if (0 == Res){QLOG_INFO() << "KepServer已链接";opcTool.openflag = true;opcTool.InitSignal(opcclient);QLOG_INFO() << "初始化信号";opcTool.addConnectMonitoredItemToVariable(opcclient_recvHeart);opcTool.addControlMonitoredItemToVariable(opcclient_cam);plc_sendHeart->set_flag(false);plc_sendHeart->start();QLOG_INFO() << "开启心跳发送";plc_recvHeart->set_flag(false);plc_recvHeart->clearheart();plc_recvHeart->start();QLOG_INFO() << "开启心跳接收";}else{QLOG_WARN() << "KepServer链接失败";opcTool.openflag = false;}
}

注册回调函数,接收数据

void OpcTool::addControlMonitoredItemToVariable(UA_Client* client)
{UA_CreateSubscriptionRequest request = UA_CreateSubscriptionRequest_default();UA_CreateSubscriptionResponse response = UA_Client_Subscriptions_create(client, request,NULL, NULL, NULL);UA_UInt32 subId = response.subscriptionId;static UA_MonitoredItemCreateRequest items[5];static UA_Client_DataChangeNotificationCallback callbacks[5];static UA_Client_DeleteMonitoredItemCallback deleteCallbacks[5];static UA_NodeId* contexts[5];items[0] = UA_MonitoredItemCreateRequest_default(camGarb);callbacks[0] = handler_ControlDataChanged;contexts[0] = &camGarb;deleteCallbacks[0] = NULL;UA_CreateMonitoredItemsRequest createRequest;UA_CreateMonitoredItemsRequest_init(&createRequest);createRequest.subscriptionId = subId;createRequest.timestampsToReturn = UA_TIMESTAMPSTORETURN_BOTH;createRequest.itemsToCreate = items;createRequest.itemsToCreateSize = 1;//个数,需根据上面信号个数修改UA_CreateMonitoredItemsResponse createResponse =UA_Client_MonitoredItems_createDataChanges(client, createRequest, (void**)contexts,callbacks, deleteCallbacks);
}

需要注册的回调函数,通知指定地址值变化

void OpcTool::handler_ControlDataChanged(UA_Client* client, UA_UInt32 subID,void* subContext, UA_UInt32 monID,void* monContext, UA_DataValue* value)
{// 回调函数UA_NodeId* ptr = (UA_NodeId*)monContext; // Monitored Context当客户端连接的服务器的任意值变化,读取数据到ptr地址if (UA_NodeId_equal(ptr, &camGarb)) // 判断ptr地址和需要监听的地址是否一致{UA_Int16 currentValue = *(UA_Int16*)(value->value.data);testsignal = currentValue;camsignals[0] = currentValue;}
}

数据发送函数封装

void OpcTool::SendBoolSignal(UA_Client*& opcclient, UA_NodeId* target, bool flag)
{UA_StatusCode status;UA_Variant Send;UA_Variant_init(&Send);UA_Boolean signal = flag;UA_Variant_setScalar(&Send, &signal, &UA_TYPES[UA_TYPES_BOOLEAN]);status = UA_Client_writeValueAttribute(opcclient, *target, &Send);if (status != UA_STATUSCODE_GOOD){UA_Client_delete(opcclient); /* Disconnects the client internally */opcclient = UA_Client_new();UA_ClientConfig_setDefault(UA_Client_getConfig(opcclient));ConnectOPC(opcclient, "opc.tcp://127.0.0.1:49320");UA_Client_writeValueAttribute(opcclient, *target, &Send);}
}void OpcTool::SendShortSignal(UA_Client*& opcclient, UA_NodeId* target, short num)
{UA_StatusCode status;UA_Variant Send;UA_Variant_init(&Send);UA_Int16 sendsignal = num;UA_Variant_setScalar(&Send, &sendsignal, &UA_TYPES[UA_TYPES_INT16]);status = UA_Client_writeValueAttribute(opcclient, *target, &Send);if (status != UA_STATUSCODE_GOOD){UA_Client_delete(opcclient); /* Disconnects the client internally */opcclient = UA_Client_new();UA_ClientConfig_setDefault(UA_Client_getConfig(opcclient));ConnectOPC(opcclient, "opc.tcp://127.0.0.1:49320");UA_Client_writeValueAttribute(opcclient, *target, &Send);}
}

https://www.modb.pro/db/585919重要参考,如何设置plc与上位机信号连接
https://www.cnblogs.com/qq2806933146xiaobai/p/16724863.html kepserver信号接收缓慢的注意事项
https://www.cnblogs.com/winformasp/articles/12534003.html 对kepserver更详细讲解的网站

http://www.xdnf.cn/news/17043.html

相关文章:

  • 机器学习Adaboost算法----SAMME算法和SAMME.R算法
  • 【2025年8月5日】将运行一段时间的单机MongoDB平滑迁移至副本集集群
  • LeetCode算法日记 - Day 2: 快乐数、盛水最多容器
  • 计算机常用英语词汇大全
  • 【unitrix】1.1 readme.md
  • Erdős–Rényi (ER) 模型
  • Android10 系统休眠调试相关
  • 文件编译、调试及库制作
  • 视频水印技术中的变换域嵌入方法对比分析
  • 从 “看懂图” 到 “读懂视频”:多模态技术如何用文本反哺视觉?
  • FPGA实现Aurora 8B10B视频点对点传输,基于GTP高速收发器,提供4套工程源码和技术支持
  • RC和RR的区别
  • 关于npx react-native run-android下载进程缓慢以及进程卡壳等问题的解决方案。
  • iouring系统调用及示例
  • 16核32G硬件服务器租用需要多少钱
  • 【安卓][Mac/Windows】永久理论免费 无限ip代理池 - 适合临时快速作战
  • 【数字图像处理系列笔记】Ch01:绪论
  • Vue2项目—基于路由守卫实现钉钉小程序动态更新标题
  • 20250805
  • GitCode新手使用教程
  • 初学docker
  • 基于k8s环境下的pulsar常用命令(上)
  • 【Lua】题目小练8
  • nflsoi 8.2 题解
  • Druid与JdbcTemplate基本使用
  • vscode 关闭自动更新
  • 从达梦到 StarRocks:国产数据库实时入仓实践
  • Memcached 缓存详解及常见问题解决方案
  • P1002 [NOIP 2002 普及组] 过河卒
  • 06 基于sklearn的机械学习-欠拟合、过拟合、正则化、逻辑回归、k-means算法