Sur cette page

Pompe à eau

Interagir avec un serveur OPC UA pour construire une IHM basée sur QML pour une simple pompe à eau.

L'exempleWater Pump montre comment utiliser Qt OPC UA pour interagir avec un serveur OPC UA afin de construire 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

Construction du serveur

Avant de pouvoir utiliser les exemples Water Pump, vous devez construire le serveur de simulation Water Pump. Vous pouvez l'ouvrir et le construire dans QtCreator ou à partir du terminal comme d'habitude.

La simulation

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

Les nœuds suivants existent sur le serveur :

NodeIdFonction
ns=2;s=MachineLe dossier contenant les nœuds de méthodes et de variables 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.TargetPercentLe point de consigne pour le pompage et le rinçage
ns=2;s=Machine.Tank2.ValveStateL'état de la vanne du deuxième réservoir
ns=2;s=Machine.DesignationDésignation de la machine lisible par l'homme à des fins d'affichage
ns=2;s=Machine.StartAppeler cette méthode pour démarrer la pompe
ns=2;s=Machine.StopAppeler cette méthode pour arrêter la pompe
ns=2;s=Machine.FlushTank2Appel de cette méthode pour rincer le réservoir 2
ns=2;s=Machine.ResetAppelle 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, essayer de démarrer la pompe si le premier réservoir est vide).

Caractéristiques du client

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

Mise en œuvre

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 enveloppant les appels de méthode OPC UA.

Variables membres

Un pointeur sur QOpcUaClient est nécessaire pour la gestion des connexions. Un pointeur supplémentaire vers un objet QOpcUaNode est nécessaire 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 rapporté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 changement et une propriété sont ajoutés pour permettre les liaisons de propriété 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)
    ...
Manipulateurs asynchrones

L'API asynchrone de 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 à l'aide de QOpcUaNode::attributeUpdated. Un gestionnaire connecté à ce signal reçoit la nouvelle valeur à l'adresse QVariant et peut écrire cette valeur dans une variable ou émettre un signal avec la nouvelle valeur, par exemple.

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 lorsqu'elle est terminée. Le client doit vérifier le code d'état et obtenir le résultat 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 site QOpcUaProvider est créé et les backends disponibles sont enregistrés afin de fournir un modèle pour le menu déroulant de sélection des backends.

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

Avant de tenter une connexion, un QOpcUaClient avec le backend sélectionné est créé. Son signal QOpcUaClient::endpointsRequestFinished est connecté à l'emplacement requestEndpointsFinished du backend. Le signal QOpcUaClient::stateChanged doit être connecté à l'emplacement 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; // Index 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 d'accès disponibles sur le serveur et démarre une connexion à la première entrée de la liste. S'il n'y a pas de point d'accès disponible, 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 agit lorsque QOpcUaClient est connecté ou déconnecté. En cas de connexion réussie, les variables membres de nœud créées précédemment sont remplies avec des objets de nœud.

    ...
    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 que tous les objets nœuds ont été créés, les gestionnaires de changement de données sont connectés aux objets nœuds 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 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 dispositif de réglage du point de consigne est ajouté au backend.

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

Pour les méthodes, des wrappers qui appellent 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 de backend est créée et transmise à la partie QML en tant que propriété de contexte nommée uaBackend.

    ...
    OpcUaMachineBackend backend;

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

Les propriétés, les signaux et les méthodes Q_INVOKABLE de uaBackend sont désormais accessibles au code QML. Par exemple, le bouton permettant de rincer le deuxième réservoir n'est activé que si le backend est connecté au serveur, que la machine est à l'arrêt et que le niveau du réservoir est supérieur au point de consigne. Au clic, 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 démarré automatiquement par l'application IHM. Après s'être connecté au serveur en cliquant sur le bouton Connect, faites glisser le curseur pour définir un point de consigne. Cliquez ensuite sur Start pour commencer à pomper l'eau du premier réservoir vers le second. Si vous définissez un point de consigne inférieur à la valeur actuelle du deuxième réservoir, cliquez sur Flush pour ouvrir la vanne.

S'il n'y a plus d'eau, cliquez sur Reset simulation pour remplir le premier réservoir.

Fichiers :

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.