このページでは

ウォーターポンプ

OPC UAサーバーと連携し、簡易なウォーターポンプ装置向けのQMLベースのHMIを構築します。

「ウォーターポンプ」の例では、Qt OPC UA を使用して OPC UA サーバーと連携し、単純な機械向けの QML ベースの HMI を構築する方法を示しています。

2つのタンク、ポンプ制御装置、およびステータスパネルを備えたウォーターポンプシステム

サーバーの構築

「ウォーターポンプ」サンプルを使用するには、まず「ウォーターポンプシミュレーションサーバー」をビルドする必要があります。QtCreatorで開いてビルドするか、通常通りターミナルからビルドすることができます。

シミュレーション

このサンプルに含まれるOPC UAサーバーは、2つのタンク、ウォーターポンプ、およびバルブを備えた機械のシミュレーションを実行します。 第1のタンクから第2のタンクへ水を汲み上げ、その後、バルブを開くことで第2のタンクから水を排出することができます。どちらの操作にもユーザーが設定可能なセットポイントがあり、第2のタンクへ汲み上げられる水量や排出される水量を制御することができます。

サーバー上には、以下のノードが存在します:

NodeId関数
ns=2;s=Machineそのマシン用のメソッドノードおよび変数ノードを含むフォルダ
ns=2;s=Machine.Stateマシンの状態
ns=2;s=Machine.Tank1.PercentFilled1番目のタンクの現在の充填状況
ns=2;s=Machine.Tank2.PercentFilled2番目のタンクの現在の充填状況
ns=2;s=Machine.Tank2.TargetPercentポンプ運転および洗浄のための設定値
ns=2;s=Machine.Tank2.ValveState第2タンクのバルブの状態
ns=2;s=Machine.Designation表示用の、人間が読みやすい機械の名称
ns=2;s=Machine.Startポンプを起動するには、このメソッドを呼び出します
ns=2;s=Machine.Stopポンプを停止するには、このメソッドを呼び出します
ns=2;s=Machine.FlushTank2タンク2を洗浄するには、このメソッドを呼び出します
ns=2;s=Machine.Resetシミュレーションをリセットするには、このメソッドを呼び出します

すべてのメソッドは、成功した場合はGood を返し、操作が不正な場合(例えば、第1タンクが空の状態でポンプを起動しようとした場合など)はBadUserAccessDenied を返します。

クライアント機能

この例では、読み取り、書き込み、メソッド呼び出し、およびデータ変更のサブスクリプションを使用し、QOpcUaClient およびQOpcUaNode が提供する非同期操作に対するハンドラの設定方法を示しています。

実装

バックエンドクラスを使用して、OPC UA サーバーとの通信を処理し、OPC UA メソッド呼び出しをラップするプロパティおよびQ_INVOKABLE メソッドを通じて、このサーバーのコンテンツを公開します。

メンバ変数

接続管理には、QOpcUaClient へのポインタが必要です。また、HMIがやり取りを行う各OPC UAノードごとに、QOpcUaNode オブジェクトへの追加のポインタが必要となります。これらのノードの値については、サーバーから最後に報告された値を含むメンバ変数が追加されます。

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

HMIで使用される各値について、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)
    ...
非同期ハンドラ

Qt OPC UA の非同期APIでは、すべての操作に対してシグナルハンドラが必要です。

データ変更のサブスクリプションは、QOpcUaNode::attributeUpdated を使用して更新を通知します。このシグナルに接続されたハンドラは、QVariant として新しい値を受け取り、その値を変数に代入したり、新しい値を含むシグナルを発行したりすることができます。

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

読み取り操作は、完了時に `QOpcUaNode::attributeRead ` シグナルをエミットします。クライアントはステータスコードを確認し、その後ノードから結果を取得する必要があります。

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);
        }
    }
}
サーバーとのやり取り

コンストラクタでは、QOpcUaProvider オブジェクトが作成され、利用可能なバックエンドが保存されます。これにより、バックエンド選択ドロップダウンメニューの候補が提供されます。

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

接続を試みる前に、選択されたバックエンドを持つQOpcUaClient が作成されます。そのQOpcUaClient::endpointsRequestFinished シグナルは、バックエンドのrequestEndpointsFinished スロットに接続されます。また、QOpcUaClient::stateChanged シグナルは、バックエンドのclientStateHandler スロットに接続する必要があります。

voidOpcUaMachineBackend::connectToEndpoint(constQString&url,qint32 index)
{
    if(m_connected)
        return;

    QOpcUaProvider provider;

    if(index< 0||index>=m_backends.size())
        return;// 無効なインデックス

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

OpcUaMachineBackend::requestEndpointsFinished スロットは、サーバー上で利用可能なエンドポイントのリストを受け取り、リストの最初のエントリへの接続を開始します。利用可能なエンドポイントがない場合、接続の確立は中止されます。

voidOpcUaMachineBackend::requestEndpointsFinished(constQList<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 は、QOpcUaClient が接続または切断された際に動作します。接続に成功した場合、事前に作成されたノードのメンバ変数にノードオブジェクトが格納されます。

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

すべてのノードオブジェクトが作成された後、データ変更ハンドラがノードオブジェクトに接続され、監視が有効になります。

    ...
        // 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(
    ...

マシン指定は変更される予定がなく、起動時に一度読み込まれます。

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

バックエンドに、設定値用のセッターが追加されました。

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

メソッドについては、OPC UAサーバーのメソッドを呼び出すラッパーが作成されます。

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

バックエンドインスタンスが作成され、uaBackend という名前のコンテキストプロパティとしてQML部分に渡されます。

    ...
    OpcUaMachineBackend backend;

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

これにより、QMLコードからuaBackendのプロパティ、シグナル、およびQ_INVOKABLE メソッドにアクセスできるようになります。例えば、2番目のタンクをフラッシュするボタンは、バックエンドがサーバーに接続されており、マシンがアイドル状態で、かつタンクのレベルが設定値を上回っている場合にのみ有効になります。クリックすると、サーバー上でflushTank2() メソッドが呼び出されます。

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

また、バックエンドからのシグナルもQMLコード内で直接使用できます。

    Connections {
        target: uaBackend

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

使用方法

サーバーはHMIアプリケーションによって自動的に起動されます。「Connect 」ボタンをクリックしてサーバーに接続した後、スライダーをドラッグして設定値を設定します。その後、「Start 」をクリックすると、第1タンクから第2タンクへの水の移送が開始されます。設定値を第2タンクの現在の値よりも低く設定している場合、「Flush 」をクリックするとバルブが開きます。

水がなくなったら、「Reset simulation 」をクリックして、1番目のタンクに水を補充してください。

サンプルプロジェクト @ code.qt.io

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