이 페이지에서

워터 펌프

OPC UA 서버와 연동하여 간단한 물 펌프 기계를 위한 QML 기반 HMI를 구축합니다.

‘워터 펌프’ 예제는 Qt OPC UA 를 사용하여 OPC UA 서버와 통신하고, 간단한 기계를 위한 QML 기반 HMI를 구축하는 방법을 보여줍니다.

두 개의 탱크, 펌프 제어 장치 및 상태 패널이 포함된 물 펌프 시스템

서버 구축

Water Pump 예제를 사용하기 전에 Water Pump 시뮬레이션 서버를 빌드해야 합니다. 평소와 같이 QtCreator에서 열어서 빌드하거나 터미널에서 빌드할 수 있습니다.

시뮬레이션

이 예제에 포함된 OPC UA 서버는 두 개의 탱크, 물 펌프, 밸브로 구성된 기계의 시뮬레이션을 실행합니다. 첫 번째 탱크에서 두 번째 탱크로 물을 펌핑한 다음, 밸브를 열어 두 번째 탱크에서 물을 배출할 수 있습니다. 두 작업 모두 사용자가 설정할 수 있는 설정값(setpoint)이 있어, 두 번째 탱크로 펌핑되거나 배출되는 물의 양을 제어할 수 있습니다.

서버에는 다음과 같은 노드들이 있습니다:

NodeId함수
ns=2;s=Machine해당 머신의 메서드 및 변수 노드가 포함된 폴더
ns=2;s=Machine.State머신의 상태
ns=2;s=Machine.Tank1.PercentFilled첫 번째 탱크의 현재 충전 상태
ns=2;s=Machine.Tank2.PercentFilled두 번째 탱크의 현재 충전 상태
ns=2;s=Machine.Tank2.TargetPercent펌핑 및 세척을 위한 설정값
ns=2;s=Machine.Tank2.ValveState두 번째 탱크 밸브의 상태
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 `를 반환하고, 작업이 유효하지 않은 경우(예: 첫 번째 탱크가 비어 있는 상태에서 펌프를 시작하려는 경우) ` 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 ` 슬롯에 연결되어야 합니다.

void OpcUaMachineBackend::connectToEndpoint(const QString&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 슬롯은 서버에서 사용 가능한 엔드포인트 목록을 수신하고, 목록의 첫 번째 항목에 대한 연결을 시작합니다. 사용 가능한 엔드포인트가 없는 경우, 연결 설정이 중단됩니다.

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

이제 QML 코드에서 uaBackend의 속성, 신호 및 ` Q_INVOKABLE ` 메서드에 접근할 수 있습니다. 예를 들어, 두 번째 탱크를 비우는 버튼은 백엔드가 서버에 연결되어 있고, 기계가 유휴 상태이며, 탱크 수위가 설정값보다 높을 때만 활성화됩니다. 이 버튼을 클릭하면 서버에서 ` 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 ’를 클릭하여 첫 번째 탱크에서 두 번째 탱크로 물을 펌핑하기 시작합니다. 설정값을 두 번째 탱크의 현재 값보다 낮게 지정한 경우, ‘ Flush ’를 클릭하면 밸브가 열립니다.

물이 남아 있지 않은 경우, ‘ Reset simulation ’을 클릭하여 첫 번째 탱크에 물을 다시 채우십시오.

예제 프로젝트 @ 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.