水泵

通过与 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));
}

clientStateHandlerQOpcUaClient 连接或断开连接做出反应。在成功连接的情况下,之前创建的节点成员变量被填充为节点对象。

    ...
    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 公司在芬兰和/或其他国家的商标。所有其他商标均为各自所有者的财产。