ウォーターポンプ

OPC UA サーバーと対話し、シンプルなウォーターポンプ機の QML ベースの HMI を構築する。

Water Pump の例では、Qt OPC UA を使って OPC UA サーバと対話し、シンプルな機械の QML ベースの HMI を構築する方法を示します。

サーバーの構築

Water Pump の例を使用する前に、Water Pump Simulation Server をビルドする必要があります。ビルドはQtCreatorまたはターミナルから行います。

シミュレーション

このサンプルに含まれるOPC UAサーバーは、2つのタンク、水ポンプ、バルブを含む機械のシミュレーションを実行します。水は1つ目のタンクから2つ目のタンクに汲み上げられ、バルブを開くことで2つ目のタンクから流されます。どちらの操作にも、ユーザーが設定可能な設定値があり、第2のタンクに汲み上げたり、第2のタンクから流したりする水の量を制御できる。

サーバーには以下のノードが存在する:

NodeId機能
ns=2;s=Machineマシンのメソッドノードと変数ノードを含むフォルダ
ns=2;s=Machine.Stateマシンの状態
ns=2;s=Machine.Tank1.PercentFilled最初のタンクの現在の充填状態
ns=2;s=Machine.Tank2.PercentFilled(充填率2つ目のタンクの現在の充填状態
ns=2;s=Machine.Tank2.TargetPercentポンピングとフラッシングの設定値
ns=2;s=Machine.Tank2.ValveState第2タンクのバルブの状態
ns=2;s=機械.指定表示用に人間が読み取れる機械の名称
ns=2;s=Machine.Startこのメソッドを呼び出してポンプを始動させる
ns=2;s=Machine.Stopポンプを停止するには、このメソッドを呼び出します。
ns=2;s=Machine.FlushTank2このメソッドをコールして、タンク 2 を洗浄する
ns=2;s=Machine.Resetこのメソッドを呼び出してシミュレーションをリセットする

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

クライアントの機能

この例では、読み込み、書き込み、メソッド呼び出し、データ変更サブスクリプションを使用し、QOpcUaClientQOpcUaNode で提供される非同期操作のハンドラを設定する方法を示します。

実装

バックエンドクラスは、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 スロットに接続されていなければなりません。

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

    QOpcUaProvider provider;

    if (index < 0 || index >= m_backends.size())
        return; // Invalid 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);
}

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

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 は が接続または切断されたときに動作します。接続に成功すると、前に作成されたノード・メンバ変数がノード・オブジェクトで満たされます。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);
    ...

uaBackendのプロパティ、シグナル、Q_INVOKABLE メソッドはQMLコードからアクセスできるようになります。例えば、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 をクリックして最初のタンクに水を補充します。

ファイル

Qt Quick Water Pumpも参照してください

©2024 The Qt Company Ltd. 本文書に含まれる文書の著作権は、それぞれの所有者に帰属します。 本書で提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。