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.

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:
| NodoId | Función |
|---|---|
| ns=2;s=Máquina | La carpeta que contiene los nodos de métodos y variables de la máquina |
| ns=2;s=Máquina.Estado | El estado de la máquina |
| ns=2;s=Máquina.Tanque1.PorcentajeLlenado | El estado actual de llenado del primer tanque |
| ns=2;s=Máquina.Depósito2.PorcentajeLlenado | Estado actual de llenado del segundo depósito |
| ns=2;s=Máquina.Tanque2.PorcentajeObjetivo | El punto de consigna para el bombeo y el lavado |
| ns=2;s=Máquina.Tanque2.EstadoVálvula | El estado de la válvula del segundo tanque |
| ns=2;s=Máquina.Designación | Designación legible de la máquina para su visualización |
| ns=2;s=Máquina.Arrancar | Llama a este método para arrancar la bomba |
| ns=2;s=Máquina.Parar | Llamar a este método para parar la bomba |
| ns=2;s=Máquina.PurgarDepósito2 | Llama a este método para vaciar el tanque 2 |
| ns=2;s=Máquina.Reiniciar | Llama 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:
- waterpump/waterpump-qmlcpp/CMakeLists.txt
- waterpump/waterpump-qmlcpp/Pump.qml
- waterpump/waterpump-qmlcpp/ServerControl.qml
- waterpump/waterpump-qmlcpp/Tank.qml
- waterpump/waterpump-qmlcpp/Tank1Unit.qml
- waterpump/waterpump-qmlcpp/Tank2Unit.qml
- waterpump/waterpump-qmlcpp/TankSimulation.qml
- waterpump/waterpump-qmlcpp/ValueDisplay.qml
- waterpump/waterpump-qmlcpp/main.cpp
- waterpump/waterpump-qmlcpp/main.qml
- waterpump/waterpump-qmlcpp/opcuamachinebackend.cpp
- waterpump/waterpump-qmlcpp/opcuamachinebackend.h
- waterpump/waterpump-qmlcpp/qml.qrc
- waterpump/waterpump-qmlcpp/waterpump-qmlcpp.pro
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.