Wasserpumpe
Interaktion mit einem OPC UA-Server zur Erstellung einer QML-basierten HMI für eine einfache Wasserpumpe.
Das Beispiel "Wasserpumpe " zeigt, wie man Qt OPC UA verwendet, um mit einem OPC UA-Server zu interagieren und eine QML-basierte HMI für eine einfache Maschine zu erstellen.
Aufbau des Servers
Bevor Sie die Wasserpumpen-Beispiele verwenden können, müssen Sie den Wasserpumpen-Simulationsserver erstellen. Sie können ihn im QtCreator oder wie gewohnt über das Terminal öffnen und erstellen.
Die Simulation
Der in diesem Beispiel enthaltene OPC UA Server simuliert eine Maschine mit zwei Tanks, einer Wasserpumpe und einem Ventil. Wasser kann aus dem ersten Tank in den zweiten Tank gepumpt werden und dann durch Öffnen des Ventils aus dem zweiten Tank gespült werden. Für beide Vorgänge gibt es einen vom Benutzer konfigurierbaren Sollwert, der die Steuerung der in den zweiten Tank gepumpten bzw. aus dem zweiten Tank gespülten Wassermenge ermöglicht.
Auf dem Server gibt es die folgenden Knotenpunkte:
NodeId | Funktion |
---|---|
ns=2;s=Maschine | Der Ordner, der die Methoden- und Variablenknoten für die Maschine enthält |
ns=2;s=Maschine.Zustand | Der Zustand der Maschine |
ns=2;s=Machine.Tank1.PercentFilled | Der aktuelle Füllstand des ersten Tanks |
ns=2;s=Maschine.Tank2.PercentFilled | Der aktuelle Füllstand des zweiten Tanks |
ns=2;s=Maschine.Tank2.SollProzent | Der Sollwert für das Abpumpen und Spülen |
ns=2;s=Maschine.Tank2.VentilZustand | Der Zustand des Ventils des zweiten Tanks |
ns=2;s=Machine.Designation | Eine für den Menschen lesbare Bezeichnung der Maschine zu Anzeigezwecken |
ns=2;s=Maschine.Start | Aufruf dieser Methode, um die Pumpe zu starten |
ns=2;s=Maschine.Stop | Aufruf dieser Methode, um die Pumpe zu stoppen |
ns=2;s=Machine.FlushTank2 | Aufruf dieser Methode zum Spülen von Tank 2 |
ns=2;s=Maschine.Reset | Aufruf dieser Methode, um die Simulation zurückzusetzen |
Alle Methoden geben im Erfolgsfall Good und im Falle einer unzulässigen Operation (z. B. beim Versuch, die Pumpe zu starten, wenn der erste Tank leer ist) BadUserAccessDenied zurück.
Client-Funktionen
Dieses Beispiel verwendet Lese- und Schreibzugriffe, Methodenaufrufe und Datenänderungsabonnements und zeigt, wie man Handler für die asynchronen Operationen von QOpcUaClient und QOpcUaNode einrichtet.
Implementierung
Eine Backend-Klasse wird verwendet, um die Kommunikation mit dem OPC UA Server abzuwickeln und den Inhalt dieses Servers mit Hilfe von Eigenschaften und Q_INVOKABLE
Methoden, die die OPC UA Methodenaufrufe umhüllen, offenzulegen.
Mitgliedsvariablen
Ein Zeiger auf QOpcUaClient wird für die Verbindungsverwaltung benötigt. Ein zusätzlicher Zeiger auf ein QOpcUaNode Objekt wird für jeden OPC UA Knoten benötigt, mit dem das HMI interagiert. Für die Werte dieser Knoten werden Membervariablen hinzugefügt, die den letzten 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 Wert, der in der HMI verwendet wird, werden ein Getter, ein geändertes Signal 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 Signalhandler für alle Operationen.
Datenänderungsabonnements melden ihre Aktualisierungen mit QOpcUaNode::attributeUpdated. Ein Handler, der mit diesem Signal verbunden ist, erhält den neuen Wert als QVariant und kann diesen Wert z. B. 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); }
Ein Lesevorgang gibt nach Abschluss das Signal QOpcUaNode::attributeRead aus. Der Client muss den Statuscode prüfen und dann 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 eine 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. Das Signal QOpcUaClient::endpointsRequestFinished wird mit dem Slot requestEndpointsFinished
des Backends verbunden. Das QOpcUaClient::stateChanged -Signal muss mit dem clientStateHandler
-Slot 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 verfügbaren Endpunkte auf dem Server und startet eine Verbindung zum ersten Eintrag in der Liste. Wenn keine Endpunkte verfügbar sind, 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 auf QOpcUaClient, wenn die Verbindung hergestellt oder unterbrochen wird. Im Falle einer erfolgreichen Verbindung werden die zuvor erstellten Knotenvariablen 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änderungshandler mit den Knotenobjekten verbunden und die Überwachung wird 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 einmalig beim Start gelesen.
... // 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); ...
Im Backend wird ein Setzer 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 Server Methode aufrufen.
void OpcUaMachineBackend::startPump() { m_machineNode->callMethod(u"ns=2;s=Machine.Start"_s); }
Die HMI
Es wird eine Backend-Instanz erstellt und dem QML-Teil als Kontexteigenschaft namens uaBackend
übergeben.
... OpcUaMachineBackend backend; QQmlApplicationEngine engine; engine.rootContext()->setContextProperty("uaBackend", &backend); ...
Die Eigenschaften, Signale und Q_INVOKABLE
Methoden von uaBackend können nun vom QML-Code angesprochen werden. Zum Beispiel wird die Schaltfläche zum Spülen des zweiten Tanks nur aktiviert, wenn das Backend mit dem Server verbunden ist, die Maschine im Leerlauf ist und der Tankfüllstand über dem Sollwert liegt. Beim Anklicken 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 auch 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 einzustellen. Klicken Sie dann auf Start, um mit dem Umpumpen von Wasser aus dem ersten Tank in den zweiten Tank zu beginnen. Wenn Sie einen Sollwert einstellen, der niedriger ist als der aktuelle Wert des zweiten Tanks, 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.
Dateien:
Siehe auch Qt Quick Wasserpumpe.
© 2025 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.