水泵
通过与 OPC UA 服务器交互来构建一个基于 QML 的简单水泵机器 HMI。
水泵 示例展示了如何使用 Qt OPC UA 与 OPC UA 服务器交互,以构建一个基于 QML 的简单机器的 HMI。
构建服务器
在你能够使用水泵示例之前,你需要构建 水泵模拟服务器。你可以在 QtCreator 中打开和构建它,或者在终端中像通常那样执行。
模拟
此示例中包含的 OPC UA 服务器运行一个包含两个水箱、水泵和阀门的机器的模拟。水可以从第一个水箱泵入第二个水箱,然后通过打开阀门从第二个水箱中排出。这两个操作都有一个用户可配置的设定点,允许控制泵入或从第二个水箱排出的水量。
服务器上存在以下节点
节点的 ID | 功能 |
---|---|
ns=2;s=Machine | 包含机器的方法和变量节点的文件夹 |
ns=2;s=Machine.State | 机器的状态 |
ns=2;s=Machine.Tank1.PercentFilled | 第一个水箱的当前填充状态 |
ns=2;s=Machine.Tank2.PercentFilled | 第二个水箱的当前填充状态 |
ns=2;s=Machine.Tank2.TargetPercent | 泵送和冲刷的设定点 |
ns=2;s=Machine.Tank2.ValveState | 第二个水箱阀门的当前状态 |
ns=2;s=Machine.Designation | 用于显示的机器的人类可读说明 |
ns=2;s=Machine.Start | 调用此方法以启动水泵 |
ns=2;s=Machine.Stop | 调用此方法以停止水泵 |
ns=2;s=Machine.FlushTank2 | 调用此方法以冲刷第二个水箱 |
ns=2;s=Machine.Reset | 调用此方法以重置模拟 |
如果操作成功,所有方法都返回 Good,如果操作不合法(例如,如果第一个水箱为空而尝试启动水泵),则返回 BadUserAccessDenied。
客户端功能
此示例使用了读取、写入、方法调用和数据更改订阅,并展示了如何为 QOpcUaClient 和 QOpcUaNode 提供的异步操作设置处理程序。
实现
使用后端类来处理与 OPC UA 服务器的通信,并通过属性和 Q_INVOKABLE
方法将 OPC UA 方法调用包装起来,以公开此服务器的内容。
成员变量
连接管理需要指向 QOpcUaClient 的指针。与每个与 HMI 交互的 OPC UA 节点,需要额外的指向 QOpcUaNode 对象的指针。为这些节点的值,添加了包含由服务器报告的最后值的成员变量。
... QScopedPointer<QOpcUaClient> m_client; QScopedPointer<QOpcUaNode> m_machineStateNode; QScopedPointer<QOpcUaNode> m_percentFilledTank1Node; QScopedPointer<QOpcUaNode> m_percentFilledTank2Node; QScopedPointer<QOpcUaNode> m_tank2TargetPercentNode; QScopedPointer<QOpcUaNode> m_tank2ValveStateNode; QScopedPointer<QOpcUaNode> m_machineNode; QScopedPointer<QOpcUaNode> m_machineDesignationNode; double m_percentFilledTank1; double m_percentFilledTank2; double m_tank2TargetPercent; bool m_tank2ValveState; MachineState m_machineState; QString m_machineDesignation; ...
对于 HMI 中使用的每个值,添加了获取器、变化信号和属性以启用 QML 中的属性绑定。
... Q_PROPERTY(double percentFilledTank1 READ percentFilledTank1 NOTIFY percentFilledTank1Changed) Q_PROPERTY(double percentFilledTank2 READ percentFilledTank2 NOTIFY percentFilledTank2Changed) Q_PROPERTY(double tank2TargetPercent READ tank2TargetPercent NOTIFY tank2TargetPercentChanged) Q_PROPERTY(OpcUaMachineBackend::MachineState machineState READ machineState NOTIFY machineStateChanged) Q_PROPERTY(bool tank2ValveState READ tank2ValveState NOTIFY tank2ValveStateChanged) Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged) Q_PROPERTY(QString machineDesignation READ machineDesignation NOTIFY machineDesignationChanged) Q_PROPERTY(QString message READ message NOTIFY messageChanged) ...
异步处理程序
Qt OPC UA 的异步 API 需要为所有操作提供信号处理程序。
数据更改订阅使用 QOpcUaNode::attributeUpdated 报告其更新。连接到此信号的处理器获取新的值作为 QVariant,可以将该值写入变量或通过使用新值发射信号,例如。
void OpcUaMachineBackend::percentFilledTank1Updated(QOpcUa::NodeAttribute attr, const QVariant &value) { Q_UNUSED(attr); m_percentFilledTank1 = value.toDouble(); emit percentFilledTank1Changed(m_percentFilledTank1); }
读取操作在完成时发射 QOpcUaNode::attributeRead 信号。客户端必须检查状态代码,然后从节点获取结果。
void OpcUaMachineBackend::machineDesignationRead(QOpcUa::NodeAttributes attr) { if (attr & QOpcUa::NodeAttribute::Value) { // Make sure the value attribute has been read if (m_machineDesignationNode->attributeError(QOpcUa::NodeAttribute::Value) == QOpcUa::UaStatusCode::Good) { // Make sure there was no error m_machineDesignation = m_machineDesignationNode->attribute(QOpcUa::NodeAttribute::Value).toString(); // Get the attribute from the cache emit machineDesignationChanged(m_machineDesignation); } } }
与服务器的交互
在构造函数中创建一个 QOpcUaProvider,并将可用的后端保存到提供后端选择下拉菜单的模型。
... QOpcUaProvider provider; setBackends(provider.availableBackends()); ...
在尝试建立连接之前,创建一个具有选定后端的 QOpcUaClient。将其 QOpcUaClient::endpointsRequestFinished 信号连接到后端的 requestEndpointsFinished
槽。必须将 QOpcUaClient::stateChanged 信号连接到后端的 clientStateHandler
槽。
void OpcUaMachineBackend::connectToEndpoint(const QString &url, qint32 index) { if (m_connected) return; QOpcUaProvider provider; if (index < 0 || index >= m_backends.size()) return; // Invalid index if (!m_client || (m_client && m_client->backend() != m_backends.at(index))) { m_client.reset(provider.createClient(m_backends.at(index))); if (m_client) { QObject::connect(m_client.data(), &QOpcUaClient::endpointsRequestFinished, this, &OpcUaMachineBackend::requestEndpointsFinished); QObject::connect(m_client.data(), &QOpcUaClient::stateChanged, this, &OpcUaMachineBackend::clientStateHandler); } } if (!m_client) { qWarning() << "Could not create client"; m_successfullyCreated = false; return; } m_successfullyCreated = true; m_client->requestEndpoints(url); }
OpcUaMachineBackend::requestEndpointsFinished
槽接收服务器上可用端点的列表,并开始连接到列表中的第一条记录。如果没有可用的端点,则取消连接建立。
void OpcUaMachineBackend::requestEndpointsFinished(const QList<QOpcUaEndpointDescription> &endpoints) { if (endpoints.isEmpty()) { qWarning() << "The server did not return any endpoints"; clientStateHandler(QOpcUaClient::ClientState::Disconnected); return; } m_client->connectToEndpoint(endpoints.at(0)); }
clientStateHandler
对 QOpcUaClient 连接或断开连接做出反应。在成功连接的情况下,之前创建的节点成员变量被填充为节点对象。
... if (state == QOpcUaClient::ClientState::Connected) { setMessage(u"Connected"_s); // Create node objects for reading, writing and subscriptions m_machineNode.reset(m_client->node(u"ns=2;s=Machine"_s)); m_machineStateNode.reset(m_client->node(u"ns=2;s=Machine.State"_s)); m_percentFilledTank1Node.reset(m_client->node(u"ns=2;s=Machine.Tank1.PercentFilled"_s)); m_percentFilledTank2Node.reset(m_client->node(u"ns=2;s=Machine.Tank2.PercentFilled"_s)); m_tank2TargetPercentNode.reset(m_client->node(u"ns=2;s=Machine.Tank2.TargetPercent"_s)); m_tank2ValveStateNode.reset(m_client->node(u"ns=2;s=Machine.Tank2.ValveState"_s)); m_machineDesignationNode.reset(m_client->node(u"ns=2;s=Machine.Designation"_s)); ...
在所有节点对象创建后,将数据更改处理器连接到节点对象,并启用监控。
... // Connect signal handlers for subscribed values QObject::connect(m_machineStateNode.data(), &QOpcUaNode::dataChangeOccurred, this, &OpcUaMachineBackend::machineStateUpdated); QObject::connect(m_percentFilledTank1Node.data(), &QOpcUaNode::dataChangeOccurred, this, &OpcUaMachineBackend::percentFilledTank1Updated); QObject::connect(m_percentFilledTank2Node.data(), &QOpcUaNode::dataChangeOccurred, this, &OpcUaMachineBackend::percentFilledTank2Updated); QObject::connect(m_tank2TargetPercentNode.data(), &QOpcUaNode::dataChangeOccurred, this, &OpcUaMachineBackend::tank2TargetPercentUpdated); QObject::connect(m_tank2ValveStateNode.data(), &QOpcUaNode::dataChangeOccurred, this, &OpcUaMachineBackend::tank2ValveStateUpdated); // Subscribe to data changes m_machineStateNode->enableMonitoring( QOpcUa::NodeAttribute::Value, QOpcUaMonitoringParameters(100)); m_percentFilledTank1Node->enableMonitoring( QOpcUa::NodeAttribute::Value, QOpcUaMonitoringParameters(100)); m_percentFilledTank2Node->enableMonitoring( QOpcUa::NodeAttribute::Value, QOpcUaMonitoringParameters(100)); m_tank2TargetPercentNode->enableMonitoring( QOpcUa::NodeAttribute::Value, QOpcUaMonitoringParameters(100)); m_tank2ValveStateNode->enableMonitoring( ...
机器标识不应该更改,将在启动时读取一次。
... // Connect the handler for async reading QObject::connect(m_machineDesignationNode.data(), &QOpcUaNode::attributeRead, this, &OpcUaMachineBackend::machineDesignationRead); // Request the value attribute of the machine designation node m_machineDesignationNode->readAttributes(QOpcUa::NodeAttribute::Value); ...
向后端添加了设定点的设置器。
void OpcUaMachineBackend::machineWriteTank2TargetPercent(double value) { if (m_tank2TargetPercentNode) m_tank2TargetPercentNode->writeAttribute(QOpcUa::NodeAttribute::Value, value); }
为方法创建了调用 OPC UA 服务器方法的包装器。
void OpcUaMachineBackend::startPump() { m_machineNode->callMethod(u"ns=2;s=Machine.Start"_s); }
HMI
创建后端实例并将其作为名为 uaBackend
的上下文属性传递给 QML 部分。
... OpcUaMachineBackend backend; QQmlApplicationEngine engine; engine.rootContext()->setContextProperty("uaBackend", &backend); ...
现在可以通过 QML 代码访问 uaBackend 的属性、信号和 Q_INVOKABLE
方法。例如,只有当后端连接到服务器、机器处于空闲状态且液位高于设定点时,才能启用清除第二个储罐的按钮。单击时,在服务器上调用 flushTank2()
方法。
Button { id: flushButton text: "Flush" enabled: uaBackend.connected && uaBackend.machineState === OpcUaMachineBackend.MachineState.Idle && uaBackend.percentFilledTank2 > uaBackend.tank2TargetPercent onClicked: { uaBackend.flushTank2() } }
后端信号也可以直接在 QML 代码中使用。
Connections { target: uaBackend function onPercentFilledTank2Changed(value) { if (uaBackend.machineState === OpcUaMachineBackend.MachineState.Pumping) rotation += 15 } }
用法
HMI 应用程序自动启动服务器。单击 连接 按钮连接到服务器后,拖动滑块设置设定点。然后,单击 启动 开始从第一个储罐向第二个储罐泵水。如果您将设定点设置为小于第二个储罐当前值的值,则单击 清除 打开阀门。
如果没有剩余的水,则单击 重置模拟 以重新填充第一个储罐。
文件
另请参阅 Qt 快速水泵.
© 2024 Qt 公司有限公司。本文件中包含的文档贡献均为各自所有者的版权。所提供的文档是根据自由软件基金会发布的 GNU 自由文档许可证版本 1.3 许可的。Qt 及其相关标志是 Qt 公司在芬兰和/或其他国家的商标。所有其他商标均为各自所有者的财产。