En esta página

Bomba de agua

Interacción con un servidor OPC UA para construir una HMI basada en QML para una sencilla máquina de bombeo de agua.

El ejemplo de laBomba de Agua muestra cómo utilizar Qt OPC UA para interactuar con un servidor OPC UA para construir una HMI basada en QML para una máquina simple.

Aplicación de bomba de agua con dos depósitos, controles de bomba y panel de estado

Construir el servidor

Antes de que puedas usar los ejemplos de la Bomba de Agua, necesitas construir el Servidor de Simulación de la Bomba de Agua. Puedes abrirlo y construirlo en QtCreator o desde el terminal como de costumbre.

La simulación

El servidor OPC UA incluido en este ejemplo ejecuta una simulación de una máquina que contiene dos tanques, una bomba de agua y una válvula. El agua puede ser bombeada desde el primer tanque al segundo tanque y luego ser descargada desde el segundo tanque abriendo la válvula. Ambas operaciones tienen un valor de consigna configurable por el usuario, lo que permite controlar la cantidad de agua bombeada al segundo depósito o expulsada de él.

En el servidor existen los siguientes nodos:

NodoIdFunción
ns=2;s=MáquinaLa carpeta que contiene los nodos de métodos y variables de la máquina
ns=2;s=Máquina.EstadoEl estado de la máquina
ns=2;s=Máquina.Tanque1.PorcentajeLlenadoEl estado actual de llenado del primer tanque
ns=2;s=Máquina.Depósito2.PorcentajeLlenadoEstado actual de llenado del segundo depósito
ns=2;s=Máquina.Tanque2.PorcentajeObjetivoEl punto de consigna para el bombeo y el lavado
ns=2;s=Máquina.Tanque2.EstadoVálvulaEl estado de la válvula del segundo tanque
ns=2;s=Máquina.DesignaciónDesignación legible de la máquina para su visualización
ns=2;s=Máquina.ArrancarLlama a este método para arrancar la bomba
ns=2;s=Máquina.PararLlamar a este método para parar la bomba
ns=2;s=Máquina.PurgarDepósito2Llama a este método para vaciar el tanque 2
ns=2;s=Máquina.ReiniciarLlama a este método para reiniciar la simulación

Todos los métodos devuelven Good en caso de éxito y BadUserAccessDenied si la operación es ilegal (por ejemplo, intentar arrancar la bomba si el primer tanque está vacío).

Características del cliente

Este ejemplo utiliza suscripciones de lectura, escritura, llamadas a métodos y cambio de datos y muestra cómo configurar manejadores para las operaciones asíncronas ofrecidas por QOpcUaClient y QOpcUaNode.

Implementación

Se utiliza una clase backend para manejar la comunicación con el servidor OPC UA y exponer el contenido de este servidor mediante propiedades y métodos Q_INVOKABLE que envuelven las llamadas a métodos OPC UA.

Variables miembro

Se requiere un puntero a QOpcUaClient para la gestión de la conexión. Se necesita un puntero adicional a un objeto QOpcUaNode para cada nodo OPC UA con el que interactúa la HMI. Para los valores de estos nodos, se añaden variables miembro que contienen el último valor notificado por el servidor.

    ...
    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;
    ...

Para cada valor utilizado en la HMI, se añade un getter, una señal de cambio y una propiedad para permitir la vinculación de propiedades en 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)
    ...
Manejadores asíncronos

La API asíncrona de Qt OPC UA requiere manejadores de señales para todas las operaciones.

Las suscripciones de cambio de datos informan de sus actualizaciones mediante QOpcUaNode::attributeUpdated. Un manejador conectado a esta señal obtiene el nuevo valor como QVariant y puede escribir ese valor en una variable o emitir una señal con el nuevo valor, por ejemplo.

void OpcUaMachineBackend::percentFilledTank1Updated(QOpcUa::NodeAttribute attr, const QVariant &value)
{
    Q_UNUSED(attr);
    m_percentFilledTank1 = value.toDouble();
    emit percentFilledTank1Changed(m_percentFilledTank1);
}

Una operación de lectura emite la señal QOpcUaNode::attributeRead al finalizar. El cliente tiene que comprobar el código de estado y luego obtener el resultado del nodo.

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);
        }
    }
}
Interacción con el servidor

En el constructor, se crea un QOpcUaProvider y se guardan los backends disponibles para proporcionar un modelo para el menú desplegable de selección de backends.

    ...
    QOpcUaProvider provider;
    setBackends(provider.availableBackends());
    ...

Antes de intentar una conexión, se crea un QOpcUaClient con el backend seleccionado. Su señal QOpcUaClient::endpointsRequestFinished se conecta a la ranura requestEndpointsFinished del backend. La señal QOpcUaClient::stateChanged debe estar conectada a la ranura clientStateHandler del backend.

void OpcUaMachineBackend::connectToEndpoint(const QString&url, qint32 index) { if (m_connected) return;    QOpcUaProvider provider; if (index < 0 || index >= m_backends.size()) return; // Índice no válido 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); }

La ranura OpcUaMachineBackend::requestEndpointsFinished recibe la lista de endpoints disponibles en el servidor e inicia una conexión con la primera entrada de la lista. Si no hay puntos finales disponibles, se aborta el establecimiento de la conexión.

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 actúa sobre QOpcUaClient estando conectado o desconectado. En caso de una conexión exitosa, las variables miembro del nodo creadas anteriormente se llenan con objetos nodo.

    ...
    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));
    ...

Una vez creados todos los objetos de nodo, los manejadores de cambio de datos se conectan a los objetos de nodo y se activa la monitorización.

    ...
        // 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(
    ...

La designación de la máquina no debe cambiar y se leerá una vez al inicio.

    ...
        // 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);
    ...

En el backend se añade un setter para la consigna.

void OpcUaMachineBackend::machineWriteTank2TargetPercent(double value)
{
    if (m_tank2TargetPercentNode)
        m_tank2TargetPercentNode->writeAttribute(QOpcUa::NodeAttribute::Value, value);
}

Para los métodos, se crean envoltorios que llaman al método del servidor OPC UA.

void OpcUaMachineBackend::startPump()
{
    m_machineNode->callMethod(u"ns=2;s=Machine.Start"_s);
}
La HMI

Se crea una instancia de backend y se entrega a la parte QML como una propiedad de contexto denominada uaBackend.

    ...
    OpcUaMachineBackend backend;

    QQmlApplicationEngine engine;
    engine.rootContext()->setContextProperty("uaBackend", &backend);
    ...

Ahora el código QML puede acceder a las propiedades, señales y métodos Q_INVOKABLE de uaBackend. Por ejemplo, el botón para vaciar el segundo depósito sólo se activa si el backend está conectado al servidor, la máquina está inactiva y el nivel del depósito está por encima del valor de consigna. Al hacer clic, se llama al método flushTank2() en el servidor.

        Button {
            id: flushButton
            text: "Flush"
            enabled: uaBackend.connected
                     && uaBackend.machineState === OpcUaMachineBackend.MachineState.Idle
                     && uaBackend.percentFilledTank2 > uaBackend.tank2TargetPercent
            onClicked: {
                uaBackend.flushTank2()
            }
        }

Las señales del backend también pueden utilizarse directamente en el código QML.

    Connections {
        target: uaBackend

        function onPercentFilledTank2Changed(value) {
            if (uaBackend.machineState === OpcUaMachineBackend.MachineState.Pumping)
                rotation += 15
        }
    }

Utilización

La aplicación HMI inicia automáticamente el servidor. Tras conectarse al servidor haciendo clic en el botón Connect, arrastre el control deslizante para establecer un valor de consigna. A continuación, haga clic en Start para comenzar a bombear agua del primer depósito al segundo. Si establece un valor de consigna inferior al valor actual del segundo depósito, al hacer clic en Flush se abrirá la válvula.

Si no queda agua, haga clic en Reset simulation para rellenar el primer depósito.

Ficheros:

Véase también Qt Quick Bomba de agua.

© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.