Qt+OPC开发笔记(三):OPC客户端订阅特点消息的Demo
前言
本篇介绍opc客户端订阅消息,实现一个opc事件的订阅,当订阅的数据在服务器发生变化是,客户端能立即得到更新。
Demo

OPC客户端
OPC 客户端是一种利用OPC(OLE for Process Control)协议与 OPC 服务器进行通信的软件应用程序。
功能特点
- 数据访问:提供一套简单易用的 API,使开发人员能轻松地创建、读取、更新和删除OPC服务器上的数据项,可从传感器、PLC、DCS 系统、过程分析仪等各种数据源获取实时数据。
- 事件订阅(当前使用):支持实时数据变化订阅,当服务器端的数据发生变化时,客户端能够立即获取到更新,以便及时响应和处理数据变化。
- 连接管理:负责建立和管理与 OPC 服务器的连接,包括连接的建立、监控连接状态以及在发生异常时进行重连或断开。
- 数据展示与处理:允许用户创建和管理数据视图,通常以表格或图形的方式展示实时数据流,还能对采集到的数据进行分析、存储、归档等处理,为决策提供支持。
数据访问方式
OPC 协议支持多种数据访问方式,以满足不同的应用场景需求:
- 同步访问:客户端发送请求后会一直等待,直到服务器返回响应。这种方式适用于对实时性要求较高的场景,但如果服务器响应时间较长,可能会导致客户端程序阻塞。
- 异步访问:客户端发送请求后不会等待服务器响应,而是继续执行后续操作。当服务器处理完请求后,会通过回调函数通知客户端。这种方式可以提高客户端程序的效率,避免阻塞。
- 订阅访问(当前使用):客户端可以订阅特定的数据项,当这些数据项的值发生变化时,服务器会主动将更新后的数据推送给客户端。这种方式适用于需要实时监控数据变化的场景。
订阅服务器某个消息
步骤一:连接服务器

步骤二:创建订阅


步骤三:创建监听项

步骤四:处理回调函数
这里是通过subId与监控id对应来确定是哪一个变量变化。

步骤五:Qt兼容使用定时器定时调用

Demo关键源码
创建订阅和监控项
bool OpcClientManager::createSubscriptionResponse()
{
/*
OPC UA中的订阅是异步的。也就是说,客户端向服务器发送多个PublishRequest。
服务器返回带有通知的PublishResponses。但只有在生成通知时。客户端不会等待响应,而是继续正常操作。
请注意订阅和受监视项目之间的区别。订阅用于报告通知。
MonitoredItems用于生成通知。每个MonitoredItem只附加到一个订阅。订阅可以包含许多受监视的项目。
客户端在后台自动处理PublishResponses(带回调),并在传输中保留足够的PublishRequests。
ublishResponses可以在同步服务调用期间或在“UA_Client_run_iterate”中接收
*/
// 步骤一:创建一个默认的订阅请求对象(有订阅再开放)
_subscriptionRequest = UA_CreateSubscriptionRequest_default();
_subscriptionRequest.requestedPublishingInterval = 1000; // 设置发布间隔为1000毫秒,即每秒发布一次数据
_subscriptionRequest.requestedLifetimeCount = 300; // 设置生命周期计数为300,即服务器在300个发布周期后会终止该订阅
_subscriptionRequest.requestedMaxKeepAliveCount = 10; // 设置最大保持活动计数为10,即服务器在10个发布周期内没有数据变化时,仍会发送空的通知以保持连接活跃
_subscriptionRequest.maxNotificationsPerPublish = 0; // 设置每个发布周期的最大通知数为0,表示不限制通知数量
_subscriptionRequest.publishingEnabled = true; // 启用发布功能,允许服务器主动推送数据
_subscriptionRequest.priority = 0; // 设置订阅的优先级为0,数值越高优先级越高
// 步骤二:设置订阅回复,设置状态改变通知回调和删除订阅回调
_subscriptionResponse = UA_Client_Subscriptions_create(_pUAClient,
_subscriptionRequest,
NULL,
OpcClientManager::statusChangeNotificationCallback,
OpcClientManager::deleteSubscriptionCallback);
if(_subscriptionResponse.responseHeader.serviceResult != UA_STATUSCODE_GOOD)
{
LOG << QString("Failed to UA_Client_Subscriptions_create, error code: 0x%1")
.arg(UA_StatusCode_name(_subscriptionResponse.responseHeader.serviceResult));
return false;
}
LOG << "Succeed to UA_Client_Subscriptions_create, id:" << _subscriptionResponse.subscriptionId;
startTimer(100);
return true;
}
bool OpcClientManager::createMonitoredItemRequest(int ns, int i)
{
// 前置:有一个订阅实例
// 步骤三:创建监控项请求,需要传入监控的节点
LOG << ns << i;
UA_NodeId nodeId = UA_NODEID_NUMERIC(ns, i);
UA_MonitoredItemCreateRequest monitoredItemCreateRequest = UA_MonitoredItemCreateRequest_default(nodeId);
monitoredItemCreateRequest.requestedParameters.samplingInterval = 100; // 采样间隔(单位:毫秒),指定服务器多久读取一次被监控变量的实际值。
monitoredItemCreateRequest.requestedParameters.discardOldest = true; // 当监控项的队列(Queue)已满时,是否丢弃最早的数据。
monitoredItemCreateRequest.requestedParameters.queueSize = 10; // 服务器为该监控项保留的历史值队列大小。queueSize = 10 表示服务器最多保存10个未发送给客户端的值
// 添加监控项到订阅
UA_MonitoredItemCreateResult monResult = UA_Client_MonitoredItems_createDataChange(_pUAClient,
_subscriptionResponse.subscriptionId,
UA_TIMESTAMPSTORETURN_BOTH,
monitoredItemCreateRequest,
NULL,
OpcClientManager::dataChangeNotificationCallback,
NULL);
if(monResult.statusCode != UA_STATUSCODE_GOOD)
{
LOG << "监控项创建失败 error:" << QString(UA_StatusCode_name(monResult.statusCode));
return false;
}else
{
LOG << "成功监控节点 MonId: " << monResult.monitoredItemId;
return true;
}
}
回调函数
void OpcClientManager::statusChangeNotificationCallback(UA_Client *client, UA_UInt32 subId, void *subContext, UA_StatusChangeNotification *notification)
{
LOG << __FUNCTION__ << client << subId;
}
void OpcClientManager::deleteSubscriptionCallback(UA_Client *client, UA_UInt32 subId, void *subContext)
{
LOG << __FUNCTION__ << client << subId;
}
void OpcClientManager::dataChangeNotificationCallback(UA_Client *client, UA_UInt32 subId, void *subContext, UA_UInt32 monId, void *monContext, UA_DataValue *value)
{
LOG << __FUNCTION__ << client << subId;
LOG << "数据变化通知 - 监控项ID: " << monId;
if(value->hasValue && value->value.type)
{
UA_Variant *var = &value->value;
if(var->type == &UA_TYPES[UA_TYPES_BOOLEAN])
{
LOG << *static_cast<bool *>(var->data);
}else{
LOG << "other types";
}
}
}
定时器处理
void OpcClientManager::timerEvent(QTimerEvent *event)
{
if(_pUAClient)
{
UA_Client_run_iterate(_pUAClient, 100);
}
}
工程模板v1.2.0

入坑
入坑一:订阅变量后未通知
问题
订阅变量后未通知

尝试
检查代码没有发现任何问题,考虑是否有其他问题。
更换第三方单文件全代码订阅后,变化 也无通知:

使用uaexpert测试,订阅看起来是成了:

修改成5秒,发现就是5秒了,所以这里订阅是成功了。
继续考虑代码问题了,再次查看,发现可能是打印缓存的问题,Qt输出printf需要设置stdout为0:

那么这个代码是没问题的。
回到封装的代码,对比检查,发下关键性代码:


所以open ua这个代码,收到订阅通知需要跑这个循环才可以收到。
在OPC UA通信中,客户端需要持续运行并处理服务器推送的通知,而UA_Client_run_iterate函数正是用于实现这一点的关键机制。
然后查看了其他一边监听一边写入的代码,跟想象中一样,间隔写入(PS:就是单片机的单路径一样)

解决
本意是用Qt的消息循环替代:

这个靠Qt循环的不是那么准确,还需要完善这个流程,有可能处理会有2次一同处理。
Qt+OPC开发笔记(三):OPC客户端订阅特点消息的Demo的更多相关文章
- Django开发笔记三
Django开发笔记一 Django开发笔记二 Django开发笔记三 Django开发笔记四 Django开发笔记五 Django开发笔记六 1.基于类的方式重写登录:views.py: from ...
- go微服务框架kratos学习笔记三(构建单独的http或者grpc demo项目)
go微服务框架kratos学习笔记三(构建单独的http或者grpc demo项目) 前面两篇跑通了demo项目,和大概了解了kratos demo整体结构,本篇分别构建一个http和一个grpc微服 ...
- 【大型软件开发】浅谈大型Qt软件开发(三)QtActive Server如何通过COM口传递自定义结构体?如何通过一个COM口来获得所有COM接口?
前言 最近我们项目部的核心产品正在进行重构,然后又是年底了,除了开发工作之外项目并不紧急,加上加班时间混不够了....所以就忙里偷闲把整个项目的开发思路聊一下,以供参考. 鉴于接下来的一年我要进行这个 ...
- Qt+ECharts开发笔记(四):ECharts的饼图介绍、基础使用和Qt封装百分比图Demo
前言 前一篇介绍了横向柱图图.本篇将介绍基础饼图使用,并将其封装一层Qt. 本篇的demo使用隐藏js代码的方式,实现了一个饼图的基本交互方式,并预留了Qt模块对外的基础接口. Demo演示 ...
- Netty4 学习笔记之二:客户端与服务端心跳 demo
前言 在上一篇Netty demo 中,了解了Netty中的客户端和服务端之间的通信.这篇则介绍Netty中的心跳. 之前在Mina 中心跳的使用是通过继承 KeepAliveMessageFacto ...
- Qt+ECharts开发笔记(三):ECharts的柱状图介绍、基础使用和Qt封装Demo
前言 上一篇成功是EChart随着Qt窗口变化而变化,本篇将开始正式介绍柱状图介绍.基础使用,并将其封装一层Qt. 本篇的demo实现了隐藏js代码的方式,实现了一个条形图的基本交互方式,即Qt ...
- RBL开发笔记三
2014-08-26 20:06:24 今天就是在开发这个EPOLL来处理网络事件 封装较为健壮的EPOLL模型来处理基本的网络IO 1) 超时这个主题先没有弄 在开发EPOLL包括select/po ...
- Qt+ECharts开发笔记(二):Qt窗口动态调整大小,使ECharts跟随Qt窗口大小变换而变换大小
前言 上一篇将ECharts嵌入Qt中,在开始ECharts使用之前,还有一个很重要的功能,就是在窗口变换大小的时候,ECharts的图表尺寸也要跟随Qt窗口变换大小而变换大小. Demo演示 ...
- Qt+ECharts开发笔记(五):ECharts的动态排序柱状图介绍、基础使用和Qt封装Demo
前言 上一篇的demo使用隐藏js代码的方式,实现了一个饼图的基本交互方式,并预留了Qt模块对外的基础接口. 本篇的demo实现了自动排序的柱状图,实现了一个自动排序柱状图的基本交互方式,即Qt ...
- Qt+MySql开发笔记:Qt5.9.3的msvc2017x64版本编译MySql8.0.16版本驱动并Demo连接数据库测试
前言 mysql驱动版本msvc2015x32版本调好, mysql的mingw32版本的驱动上一个版本编译并测试好,有些三方库最低支持vs2017,所以只能使用msvc2017x64,基于Qt5 ...
随机推荐
- Nginx可以同时支持ipv4与 ipv6的监听
Nginx可以同时支持ipv4与 ipv6的监听,但为了一致性的考虑,新版本Nginx推荐使用分开监听,下面我们开始进入正题. 一.默认IPV4配置 下面我们先来看一看默认的ipv4配置: 二.加入i ...
- python-argparse用法简介
1. argparse介绍 argparse是Python标准库中用于解析命令行参数的模块.它提供了一种简洁而灵活的方式来处理命令行参数,包括选项(可选参数)和位置参数(必需参数) 2. argpar ...
- 在 JavaScript 中,判断一个对象是否为空有几种方法。
使用 Object.keys() 方法检查对象的键值对数量: function isObjectEmpty(obj) { return Object.keys(obj).length === 0; } ...
- springboot接口接收xml
对xml文件的操作也可以借助hutool的XmlUtil. 1. xml格式 <root> <deviceStatInfo onlineCount="64" of ...
- eolinker校验规则之 Json结构定位:返回结果校验的方法和案例(父参、子参内容校验)
如下图,订单编号的参数在data父字段内 Eolinker返参校验的写法就需要有些变化 先写Data父参,添加子字段,再写子参 预期结果不支持全局变量 可通过添加绑定,绑定前一个接口返回参数,进行匹配
- 康谋分享 | 在基于场景的AD/ADAS验证过程中,识别挑战性场景!
基于场景的验证是AD/ADAS(自动驾驶和高级驾驶辅助)系统开发过程中的重要步骤,它包括对自动化系统进行一系列预定义场景的测试.测试中包含的场景越多,尤其挑战性场景越多,人们对正在测试的AD/ADAS ...
- kettle介绍-Step之CSV Input
CSV Input/CSV 文件输入介绍 CSV 文件输入步骤主要用于将 CSV 格式的文本文件按照一定的格式输入至 流中 Step name:步骤的名称,在单一转换中,名称必须唯一 Filename ...
- php uninx时间戳转datetime对象,获取n秒前的dateime问题
当时在网上搜了下这个问题,大多方法都是定义一个函数,在里面处理,后来网官网文档看到可以这么用,记录下. php时间戳转datetime对象 var_dump(new \Datetime('@'.tim ...
- 题解:AT_arc173_a [ARC173A] Neq Number
简单二分. 思路 数位 dp 预处理和判断. init 预处理出 dp 数组,与 windy 数大致相同. 二分答案,如果 111 至 midmidmid 的 Neq 数数量大于等于 kkk,rt=m ...
- lighttools batchmode 批处理vb程序代码
完成后的图示,选择需要模拟的lighttools后,直接进行运行,点击开始模拟,即可逐个模拟完成 lighttools 连接代码: 1 Private m_ltServer As LTAPI 2 3 ...