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:

NodeIdFunktion
ns=2;s=MaschineDer Ordner, der die Methoden- und Variablenknoten für die Maschine enthält
ns=2;s=Maschine.ZustandDer Zustand der Maschine
ns=2;s=Machine.Tank1.PercentFilledDer aktuelle Füllstand des ersten Tanks
ns=2;s=Maschine.Tank2.PercentFilledDer aktuelle Füllstand des zweiten Tanks
ns=2;s=Maschine.Tank2.SollProzentDer Sollwert für das Abpumpen und Spülen
ns=2;s=Maschine.Tank2.VentilZustandDer Zustand des Ventils des zweiten Tanks
ns=2;s=Machine.DesignationEine für den Menschen lesbare Bezeichnung der Maschine zu Anzeigezwecken
ns=2;s=Maschine.StartAufruf dieser Methode, um die Pumpe zu starten
ns=2;s=Maschine.StopAufruf dieser Methode, um die Pumpe zu stoppen
ns=2;s=Machine.FlushTank2Aufruf dieser Methode zum Spülen von Tank 2
ns=2;s=Maschine.ResetAufruf 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.