Sur cette page

Pompe à eau

Interaction avec un serveur OPC UA pour créer une IHM basée sur QML destinée à une pompe à eau simple.

L'exemple «Pompe à eau » montre comment utiliser Qt OPC UA pour interagir avec un serveur OPC UA afin de créer une IHM basée sur QML pour une machine simple.

Application de pompe à eau avec deux réservoirs, commandes de pompe et panneau d'état

Configuration du serveur

Avant de pouvoir utiliser les exemples « Water Pump », vous devez compiler le serveur de simulation « Water Pump ». Vous pouvez l'ouvrir et le compiler dans QtCreator ou depuis le terminal, comme d'habitude.

La simulation

Le serveur OPC UA inclus dans cet exemple exécute une simulation d’une machine comprenant deux réservoirs, une pompe à eau et une vanne. L'eau peut être pompée du premier réservoir vers le second, puis évacuée de ce dernier en ouvrant la vanne. Ces deux opérations disposent d'une valeur de consigne configurable par l'utilisateur, ce qui permet de contrôler la quantité d'eau pompée vers le second réservoir ou évacuée de celui-ci.

Les nœuds suivants sont présents sur le serveur :

NodeIdFonction
ns=2;s=MachineLe dossier contenant les nœuds de méthode et de variable pour la machine
ns=2;s=Machine.StateL'état de la machine
ns=2;s=Machine.Tank1.PercentFilledL'état de remplissage actuel du premier réservoir
ns=2;s=Machine.Tank2.PercentFilledL'état de remplissage actuel du deuxième réservoir
ns=2;s=Machine.Tank2.TargetPercentLa valeur de consigne pour le pompage et la purge
ns=2;s=Machine.Tank2.ValveStateL'état de la vanne du deuxième réservoir
ns=2;s=Machine.DesignationUne désignation lisible par l'utilisateur de la machine à des fins d'affichage
ns=2;s=Machine.StartAppelez cette méthode pour démarrer la pompe
ns=2;s=Machine.StopAppelez cette méthode pour arrêter la pompe
ns=2;s=Machine.FlushTank2Appelez cette méthode pour vidanger le réservoir 2
ns=2;s=Machine.ResetAppelez cette méthode pour réinitialiser la simulation

Toutes les méthodes renvoient ` Good ` en cas de succès et ` BadUserAccessDenied ` si l'opération est illégale (par exemple, si l'on tente de démarrer la pompe alors que le premier réservoir est vide).

Fonctionnalités du client

Cet exemple utilise des appels de méthode de lecture et d'écriture, ainsi que des abonnements aux modifications de données, et montre comment configurer des gestionnaires pour les opérations asynchrones proposées par QOpcUaClient et QOpcUaNode.

Implémentation

Une classe backend est utilisée pour gérer la communication avec le serveur OPC UA et exposer le contenu de ce serveur au moyen de propriétés et de méthodes Q_INVOKABLE encapsulant les appels de méthodes OPC UA.

Variables membres

Un pointeur vers ` QOpcUaClient ` est nécessaire pour la gestion de la connexion. Un pointeur supplémentaire vers un objet ` QOpcUaNode ` est requis pour chaque nœud OPC UA avec lequel l’IHM interagit. Pour les valeurs de ces nœuds, des variables membres contenant la dernière valeur renvoyée par le serveur sont ajoutées.

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

Pour chaque valeur utilisée dans l’IHM, un getter, un signal de modification et une propriété sont ajoutés afin de permettre les liaisons de propriétés dans 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)
    ...
Gestionnaires asynchrones

L'API asynchrone d'Qt OPC UA. nécessite des gestionnaires de signaux pour toutes les opérations.

Les abonnements aux modifications de données signalent leurs mises à jour via QOpcUaNode::attributeUpdated. Un gestionnaire connecté à ce signal reçoit la nouvelle valeur sous la forme QVariant et peut, par exemple, l'écrire dans une variable ou émettre un signal contenant cette nouvelle valeur.

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

Une opération de lecture émet le signal « QOpcUaNode::attributeRead » une fois terminée. Le client doit vérifier le code d'état, puis récupérer le résultat auprès du nœud.

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);
        }
    }
}
Interaction avec le serveur

Dans le constructeur, un objet ` QOpcUaProvider ` est créé et les backends disponibles sont enregistrés afin de fournir un modèle pour le menu déroulant de sélection du backend.

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

Avant toute tentative de connexion, une instance de ` QOpcUaClient ` est créée avec le backend sélectionné. Son signal ` QOpcUaClient::endpointsRequestFinished ` est connecté au slot ` requestEndpointsFinished ` du backend. Le signal ` QOpcUaClient::stateChanged ` doit être connecté au slot ` clientStateHandler ` du backend.

void OpcUaMachineBackend::connectToEndpoint(const QString&url, qint32 index)
{
    if (m_connected)
        return;

    QOpcUaProvider provider;

    if (index < 0|| index >= m_backends.size())
        return; // Indice invalide

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

Le slot OpcUaMachineBackend::requestEndpointsFinished reçoit la liste des points de terminaison disponibles sur le serveur et établit une connexion avec le premier élément de la liste. S'il n'y a pas de points de terminaison disponibles, l'établissement de la connexion est interrompu.

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 réagit lorsque QOpcUaClient est connecté ou déconnecté. En cas de connexion réussie, les variables membres `node` créées précédemment sont renseignées avec des objets `node`.

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

Une fois tous les objets nœud créés, les gestionnaires de modification de données sont associés aux objets nœud et la surveillance est activée.

    ...
        // 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 désignation de la machine n'est pas censée changer et sera lue une seule fois au démarrage.

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

Un module de réglage de la valeur de consigne a été ajouté au backend.

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

Pour les méthodes, des wrappers appelant la méthode du serveur OPC UA sont créés.

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

Une instance du backend est créée et transmise à la partie QML sous la forme d’une propriété de contexte nommée ` uaBackend`.

    ...
    OpcUaMachineBackend backend;

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

Le code QML peut désormais accéder aux propriétés, signaux et méthodes Q_INVOKABLE de uaBackend. Par exemple, le bouton permettant de vider le deuxième réservoir n’est activé que si le backend est connecté au serveur, si la machine est à l’arrêt et si le niveau du réservoir est supérieur au seuil défini. Lorsqu’on clique dessus, la méthode flushTank2() est appelée sur le serveur.

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

Les signaux provenant du backend peuvent également être utilisés directement dans le code QML.

    Connections {
        target: uaBackend

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

Utilisation

Le serveur est lancé automatiquement par l’application IHM. Après vous être connecté au serveur en cliquant sur le bouton « Connect », faites glisser le curseur pour définir une consigne. Cliquez ensuite sur « Start » pour commencer à pomper l’eau du premier réservoir vers le second. Si vous définissez une consigne inférieure à la valeur actuelle du second réservoir, cliquer sur « Flush » ouvre la vanne.

S'il ne reste plus d'eau, cliquez sur « Reset simulation » pour remplir à nouveau le premier réservoir.

Exemple de projet sur code.qt.io

Voir aussi Qt Quick Pompe à eau.

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