Wasserpumpe
Interaktion mit einem OPC-UA-Server zum Aufbau einer QML-basierten HMI für eine einfache Wasserpumpe.
Das Beispiel„Wasserpumpe“ veranschaulicht, wie man mithilfe von „ Qt OPC UA “ mit einem OPC-UA-Server interagiert, um eine QML-basierte HMI für eine einfache Maschine zu erstellen.

Einrichten des Servers
Bevor Sie die Wasserpumpen-Beispiele nutzen können, müssen Sie den Wasserpumpen-Simulationsserver erstellen. Sie können ihn wie gewohnt in QtCreator oder über das Terminal öffnen und erstellen.
Die Simulation
Der in diesem Beispiel enthaltene OPC-UA-Server führt eine Simulation einer Anlage durch, die aus zwei Tanks, einer Wasserpumpe und einem Ventil besteht. Wasser kann aus dem ersten Tank in den zweiten Tank gepumpt und anschließend durch Öffnen des Ventils aus dem zweiten Tank abgelassen werden. Beide Vorgänge verfügen über einen vom Benutzer konfigurierbaren Sollwert, wodurch sich die Wassermenge steuern lässt, die in den zweiten Tank gepumpt oder aus diesem abgelassen wird.
Auf dem Server sind folgende Knoten vorhanden:
| Knoten-ID | Funktion |
|---|---|
| ns=2;s=Machine | Der Ordner, der die Methoden- und Variablenknoten für die Maschine enthält |
| ns=2;s=Machine.State | Der Zustand der Maschine |
| ns=2;s=Machine.Tank1.PercentFilled | Der aktuelle Füllstand des ersten Tanks |
| ns=2;s=Machine.Tank2.PercentFilled | Der aktuelle Füllstand des zweiten Tanks |
| ns=2;s=Machine.Tank2.TargetPercent | Der Sollwert für das Pumpen und Spülen |
| ns=2;s=Machine.Tank2.ValveState | Der Zustand des Ventils des zweiten Tanks |
| ns=2;s=Machine.Designation | Eine für Menschen lesbare Bezeichnung der Maschine zu Anzeigezwecken |
| ns=2;s=Machine.Start | Rufen Sie diese Methode auf, um die Pumpe zu starten |
| ns=2;s=Machine.Stop | Rufen Sie diese Methode auf, um die Pumpe anzuhalten |
| ns=2;s=Machine.FlushTank2 | Rufen Sie diese Methode auf, um Tank 2 zu spülen |
| ns=2;s=Machine.Reset | Rufen Sie diese Methode auf, um die Simulation zurückzusetzen |
Alle Methoden geben bei Erfolg „ Good “ zurück und „ BadUserAccessDenied “, wenn der Vorgang unzulässig ist (z. B. beim Versuch, die Pumpe zu starten, wenn der erste Tank leer ist).
Client-Funktionen
Dieses Beispiel verwendet Lese- und Schreibvorgänge, Methodenaufrufe sowie Abonnements für Datenänderungen und zeigt, wie Handler für die von QOpcUaClient und QOpcUaNode angebotenen asynchronen Vorgänge eingerichtet werden.
Implementierung
Eine Backend-Klasse wird verwendet, um die Kommunikation mit dem OPC-UA-Server abzuwickeln und den Inhalt dieses Servers mittels Eigenschaften und Q_INVOKABLE -Methoden bereitzustellen, die die OPC-UA-Methodenaufrufe umschließen.
Membervariablen
Für die Verbindungsverwaltung ist ein Zeiger auf „ QOpcUaClient “ erforderlich. Für jeden OPC-UA-Knoten, mit dem die HMI interagiert, wird ein zusätzlicher Zeiger auf ein „ QOpcUaNode “-Objekt benötigt. Für die Werte dieser Knoten werden Member-Variablen hinzugefügt, die den zuletzt vom Server gemeldeten Wert enthalten.
...
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;
...Für jeden in der HMI verwendeten Wert werden ein Getter, ein Änderungssignal und eine Eigenschaft hinzugefügt, um Eigenschaftsbindungen in QML zu ermöglichen
...
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)
...Asynchrone Handler
Die asynchrone API von „ Qt OPC UA “ erfordert Signal-Handler für alle Operationen.
Abonnements für Datenänderungen melden ihre Aktualisierungen über das Signal „ QOpcUaNode::attributeUpdated “. Ein mit diesem Signal verbundener Handler erhält den neuen Wert als „ QVariant “ und kann diesen Wert beispielsweise in eine Variable schreiben oder ein Signal mit dem neuen Wert ausgeben.
void OpcUaMachineBackend::percentFilledTank1Updated(QOpcUa::NodeAttribute attr, const QVariant &value)
{
Q_UNUSED(attr);
m_percentFilledTank1 = value.toDouble();
emit percentFilledTank1Changed(m_percentFilledTank1);
}Eine Leseoperation sendet nach Abschluss das Signal „ QOpcUaNode::attributeRead “. Der Client muss den Statuscode überprüfen und anschließend das Ergebnis vom Knoten abrufen.
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);
}
}
}Interaktion mit dem Server
Im Konstruktor wird ein ` QOpcUaProvider ` erstellt und die verfügbaren Backends werden gespeichert, um ein Modell für das Dropdown-Menü zur Backend-Auswahl bereitzustellen.
...
QOpcUaProvider provider;
setBackends(provider.availableBackends());
...Bevor eine Verbindung hergestellt wird, wird ein ` QOpcUaClient ` mit dem ausgewählten Backend erstellt. Sein Signal ` QOpcUaClient::endpointsRequestFinished ` wird mit dem Slot ` requestEndpointsFinished ` des Backends verbunden. Das Signal ` QOpcUaClient::stateChanged ` muss mit dem Slot ` clientStateHandler ` des Backends verbunden werden.
void OpcUaMachineBackend::connectToEndpoint(const QString&url, qint32 index)
{
if (m_connected)
return;
QOpcUaProvider provider;
if (index < 0|| index >= m_backends.size())
return; // Ungültiger 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);
}Der Slot „ OpcUaMachineBackend::requestEndpointsFinished “ empfängt die Liste der auf dem Server verfügbaren Endpunkte und baut eine Verbindung zum ersten Eintrag in der Liste auf. Sind keine Endpunkte verfügbar, wird der Verbindungsaufbau abgebrochen.
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 reagiert darauf, ob QOpcUaClient verbunden oder getrennt ist. Bei einer erfolgreichen Verbindung werden die zuvor angelegten Knoten-Membervariablen mit Knotenobjekten gefüllt.
...
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));
...Nachdem alle Knotenobjekte erstellt wurden, werden die Datenänderungs-Handler mit den Knotenobjekten verknüpft und die Überwachung aktiviert.
...
// 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(
...Die Maschinenbezeichnung soll sich nicht ändern und wird beim Start einmalig eingelesen.
...
// 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);
...Dem Backend wird ein Setter für den Sollwert hinzugefügt.
void OpcUaMachineBackend::machineWriteTank2TargetPercent(double value)
{
if (m_tank2TargetPercentNode)
m_tank2TargetPercentNode->writeAttribute(QOpcUa::NodeAttribute::Value, value);
}Für die Methoden werden Wrapper erstellt, die die OPC-UA-Servermethode aufrufen.
void OpcUaMachineBackend::startPump()
{
m_machineNode->callMethod(u"ns=2;s=Machine.Start"_s);
}Die HMI
Es wird eine Backend-Instanz erstellt und als Kontexteigenschaft mit dem Namen „ uaBackend “ an den QML-Teil übergeben.
...
OpcUaMachineBackend backend;
QQmlApplicationEngine engine;
engine.rootContext()->setContextProperty("uaBackend", &backend);
...Auf die Eigenschaften, Signale und Methoden von „ Q_INVOKABLE “ des „uaBackend“ kann nun über den QML-Code zugegriffen werden. Beispielsweise ist die Schaltfläche zum Entleeren des zweiten Tanks nur dann aktiviert, wenn das Backend mit dem Server verbunden ist, die Maschine im Leerlauf ist und der Tankfüllstand über dem Sollwert liegt. Beim Klicken wird die Methode „ flushTank2() “ auf dem Server aufgerufen.
Button {
id: flushButton
text: "Flush"
enabled: uaBackend.connected
&& uaBackend.machineState === OpcUaMachineBackend.MachineState.Idle
&& uaBackend.percentFilledTank2 > uaBackend.tank2TargetPercent
onClicked: {
uaBackend.flushTank2()
}
}Signale vom Backend können ebenfalls direkt im QML-Code verwendet werden.
Connections {
target: uaBackend
function onPercentFilledTank2Changed(value) {
if (uaBackend.machineState === OpcUaMachineBackend.MachineState.Pumping)
rotation += 15
}
}Verwendung
Der Server wird automatisch von der HMI-Anwendung gestartet. Nachdem Sie durch Klicken auf die Schaltfläche „ Connect “ eine Verbindung zum Server hergestellt haben, ziehen Sie den Schieberegler, um einen Sollwert festzulegen. Klicken Sie anschließend auf „ Start “, um mit dem Pumpen von Wasser aus dem ersten Tank in den zweiten Tank zu beginnen. Wenn Sie einen Sollwert festlegen, der unter dem aktuellen Wert des zweiten Tanks liegt, wird durch Klicken auf „ Flush “ das Ventil geöffnet.
Wenn kein Wasser mehr vorhanden ist, klicken Sie auf „ Reset simulation “, um den ersten Tank wieder aufzufüllen.
Siehe auch „Qt Quick Water Pump“.
© 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.