From 03672e4d7801cc927f68083f0d0bbceb82c4ed20 Mon Sep 17 00:00:00 2001 From: Rainer Keller Date: Thu, 9 May 2019 14:35:04 +0200 Subject: qml: Support for setting up a connection in C++ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change-Id: I2b00407ddfa3993e579a5d556241a5483d1529c9 Reviewed-by: Jannis Völker --- src/imports/opcua/opcuaconnection.cpp | 104 +++++++++-- src/imports/opcua/opcuaconnection.h | 8 + tests/auto/auto.pro | 2 +- tests/auto/clientSetupInCpp/clientSetupInCpp.pro | 11 ++ .../auto/clientSetupInCpp/tst_clientSetupInCpp.cpp | 191 +++++++++++++++++++++ .../auto/clientSetupInCpp/tst_clientSetupInCpp.qml | 82 +++++++++ 6 files changed, 379 insertions(+), 19 deletions(-) create mode 100644 tests/auto/clientSetupInCpp/clientSetupInCpp.pro create mode 100644 tests/auto/clientSetupInCpp/tst_clientSetupInCpp.cpp create mode 100644 tests/auto/clientSetupInCpp/tst_clientSetupInCpp.qml diff --git a/src/imports/opcua/opcuaconnection.cpp b/src/imports/opcua/opcuaconnection.cpp index 163fc14..3454520 100644 --- a/src/imports/opcua/opcuaconnection.cpp +++ b/src/imports/opcua/opcuaconnection.cpp @@ -213,6 +213,45 @@ QT_BEGIN_NAMESPACE */ +/*! + \qmlproperty QOpcUaClient Connection::connection + \since 5.13 + + This property is used only to inject a connection from C++. In case of complex setup of + a connection you can use C++ to handle all the details. After the connection is established + it can be handed to QML using this property. Ownership of the client is transferred to QML. + + \code + class MyClass : public QObject { + Q_OBJECT + Q_PROPERTY(QOpcUaClient* connection READ connection NOTIFY connectionChanged) + + public: + MyClass (QObject* parent = nullptr); + QOpcUaClient *connection() const; + + signals: + void connectionChanged(QOpcUaClient *); + \endcode + + Emitting the signal \c connectionChanged when the client setup is completed, the QML code below will + use the connection. + + \code + import QtOpcUa 5.13 as QtOpcUa + + MyClass { + id: myclass + } + + QtOpcUa.Connection { + connection: myclass.connection + } + \endcode + +*/ + + Q_DECLARE_LOGGING_CATEGORY(QT_OPCUA_PLUGINS_QML) OpcUaConnection* OpcUaConnection::m_defaultConnection = nullptr; @@ -225,10 +264,7 @@ OpcUaConnection::OpcUaConnection(QObject *parent): OpcUaConnection::~OpcUaConnection() { setDefaultConnection(false); - if (m_client) { - m_client->deleteLater(); - m_client = nullptr; - } + removeConnection(); } QStringList OpcUaConnection::availableBackends() const @@ -256,26 +292,14 @@ void OpcUaConnection::setBackend(const QString &name) if (m_client->backend() == name) return; - m_client->disconnectFromEndpoint(); - m_client->disconnect(this); - m_client->deleteLater(); + removeConnection(); } QOpcUaProvider provider; m_client = provider.createClient(name); if (m_client) { qCDebug(QT_OPCUA_PLUGINS_QML) << "Created plugin" << m_client->backend(); - connect(m_client, &QOpcUaClient::stateChanged, this, &OpcUaConnection::clientStateHandler); - connect(m_client, &QOpcUaClient::namespaceArrayUpdated, this, &OpcUaConnection::namespacesChanged); - connect(m_client, &QOpcUaClient::namespaceArrayUpdated, this, [&]() { - if (!m_connected) { - m_connected = true; - emit connectedChanged(); - } - }); - m_client->setNamespaceAutoupdate(true); - connect(m_client, &QOpcUaClient::readNodeAttributesFinished, this, &OpcUaConnection::handleReadNodeAttributesFinished); - connect(m_client, &QOpcUaClient::writeNodeAttributesFinished, this, &OpcUaConnection::handleWriteNodeAttributesFinished); + setupConnection(); } else { qCWarning(QT_OPCUA_PLUGINS_QML) << tr("Backend '%1' could not be created.").arg(name); } @@ -374,6 +398,15 @@ void OpcUaConnection::setAuthenticationInformation(const QOpcUaAuthenticationInf m_client->setAuthenticationInformation(authenticationInformation); } +void OpcUaConnection::setConnection(QOpcUaClient *client) +{ + if (!client) + return; + removeConnection(); + m_client = client; + setupConnection(); +} + QOpcUaAuthenticationInformation OpcUaConnection::authenticationInformation() const { if (!m_client) @@ -580,6 +613,11 @@ QJSValue OpcUaConnection::supportedUserTokenTypes() const return returnValue; } +QOpcUaClient *OpcUaConnection::connection() const +{ + return m_client; +} + void OpcUaConnection::handleReadNodeAttributesFinished(const QVector &results) { QVariantList returnValue; @@ -600,4 +638,34 @@ void OpcUaConnection::handleWriteNodeAttributesFinished(const QVectordisconnect(this); + m_client->disconnectFromEndpoint(); + if (!m_client->parent()) { + m_client->deleteLater(); + } + m_client = nullptr; + } +} + +void OpcUaConnection::setupConnection() +{ + connect(m_client, &QOpcUaClient::stateChanged, this, &OpcUaConnection::clientStateHandler); + connect(m_client, &QOpcUaClient::namespaceArrayUpdated, this, &OpcUaConnection::namespacesChanged); + connect(m_client, &QOpcUaClient::namespaceArrayUpdated, this, [&]() { + if (!m_connected) { + m_connected = true; + emit connectedChanged(); + } + }); + m_client->setNamespaceAutoupdate(true); + connect(m_client, &QOpcUaClient::readNodeAttributesFinished, this, &OpcUaConnection::handleReadNodeAttributesFinished); + connect(m_client, &QOpcUaClient::writeNodeAttributesFinished, this, &OpcUaConnection::handleWriteNodeAttributesFinished); + m_connected = (!m_client->namespaceArray().isEmpty() && m_client->state() == QOpcUaClient::Connected); + if (m_connected) + emit connectedChanged(); +} + QT_END_NAMESPACE diff --git a/src/imports/opcua/opcuaconnection.h b/src/imports/opcua/opcuaconnection.h index a072a65..4fdd2ed 100644 --- a/src/imports/opcua/opcuaconnection.h +++ b/src/imports/opcua/opcuaconnection.h @@ -61,6 +61,7 @@ class OpcUaConnection : public QObject Q_PROPERTY(QStringList supportedSecurityPolicies READ supportedSecurityPolicies CONSTANT) Q_PROPERTY(QJSValue supportedUserTokenTypes READ supportedUserTokenTypes CONSTANT) Q_PROPERTY(QOpcUaEndpointDescription currentEndpoint READ currentEndpoint) + Q_PROPERTY(QOpcUaClient* connection READ connection WRITE setConnection NOTIFY connectionChanged) public: OpcUaConnection(QObject *parent = nullptr); @@ -82,11 +83,14 @@ public: QStringList supportedSecurityPolicies() const; QJSValue supportedUserTokenTypes() const; + QOpcUaClient *connection() const; + public slots: void connectToEndpoint(const QOpcUaEndpointDescription &endpointDescription); void disconnectFromEndpoint(); void setDefaultConnection(bool defaultConnection = true); void setAuthenticationInformation(const QOpcUaAuthenticationInformation &authenticationInformation); + void setConnection(QOpcUaClient *client); signals: void availableBackendsChanged(); @@ -96,6 +100,7 @@ signals: void namespacesChanged(); void readNodeAttributesFinished(const QVariant &value); void writeNodeAttributesFinished(const QVariant &value); + void connectionChanged(); private slots: void clientStateHandler(QOpcUaClient::ClientState state); @@ -103,6 +108,9 @@ private slots: void handleWriteNodeAttributesFinished(const QVector &results); private: + void removeConnection(); + void setupConnection(); + QOpcUaClient *m_client = nullptr; bool m_connected = false; static OpcUaConnection* m_defaultConnection; diff --git a/tests/auto/auto.pro b/tests/auto/auto.pro index b070f81..a90406f 100644 --- a/tests/auto/auto.pro +++ b/tests/auto/auto.pro @@ -1,5 +1,5 @@ TEMPLATE = subdirs -SUBDIRS += qopcuaclient connection +SUBDIRS += qopcuaclient connection clientSetupInCpp QT_FOR_CONFIG += opcua-private diff --git a/tests/auto/clientSetupInCpp/clientSetupInCpp.pro b/tests/auto/clientSetupInCpp/clientSetupInCpp.pro new file mode 100644 index 0000000..57fb5de --- /dev/null +++ b/tests/auto/clientSetupInCpp/clientSetupInCpp.pro @@ -0,0 +1,11 @@ +TEMPLATE = app +TARGET = tst_clientSetupInCpp +CONFIG += warn_on qmltestcase +SOURCES += tst_clientSetupInCpp.cpp +HEADERS += $$PWD/../../common/backend_environment.h +INCLUDEPATH += $$PWD/../../common +IMPORTPATH += $$PWD/../../../src/plugins/declarative + +# This tries to check if the server has security support +QT += opcua_private +qtConfig(mbedtls): DEFINES += SERVER_SUPPORTS_SECURITY diff --git a/tests/auto/clientSetupInCpp/tst_clientSetupInCpp.cpp b/tests/auto/clientSetupInCpp/tst_clientSetupInCpp.cpp new file mode 100644 index 0000000..2e6eed7 --- /dev/null +++ b/tests/auto/clientSetupInCpp/tst_clientSetupInCpp.cpp @@ -0,0 +1,191 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt OPC UA module. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "backend_environment.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +const int signalSpyTimeout = 10000; +const quint16 defaultPort = 43344; +const QHostAddress defaultHost(QHostAddress::LocalHost); + +static QString envOrDefault(const char *env, QString def) +{ + return qEnvironmentVariableIsSet(env) ? qgetenv(env).constData() : def; +} + +class MyClass : public QObject +{ + Q_OBJECT + Q_PROPERTY(QOpcUaClient* connection READ connection NOTIFY connectionChanged) + +public: + MyClass (QObject* parent = nullptr) : QObject(parent) { + } + + QOpcUaClient *connection() const { + return m_client; + } + +signals: + void connectionChanged(QOpcUaClient *); + +public slots: + void startConnection() { + QOpcUaProvider p; + QOpcUaClient *client = p.createClient("open62541"); + + if (!client) + qFatal("Failed to instantiate backend"); + + m_client = client; + + QString host = envOrDefault("OPCUA_HOST", defaultHost.toString()); + QString port = envOrDefault("OPCUA_PORT", QString::number(defaultPort)); + const auto discoveryEndpoint = QString("opc.tcp://%1:%2").arg(host).arg(port); + + QSignalSpy endpointSpy(client, &QOpcUaClient::endpointsRequestFinished); + client->requestEndpoints(discoveryEndpoint); + endpointSpy.wait(signalSpyTimeout); + QCOMPARE(endpointSpy.size(), 1); + + const QVector desc = endpointSpy.at(0).at(0).value>(); + QVERIFY(desc.size() > 0); + QCOMPARE(endpointSpy.at(0).at(2).value(), discoveryEndpoint); + + client->connectToEndpoint(desc.first()); + QTRY_VERIFY_WITH_TIMEOUT(client->state() == QOpcUaClient::Connected, signalSpyTimeout); + qDebug() << "DONE"; + emit connectionChanged(m_client); + } + +private: + QOpcUaClient *m_client = nullptr; +}; + +class SetupClass : public QObject +{ + Q_OBJECT +public: + SetupClass() { + }; + ~SetupClass() { + } + +public slots: + void applicationAvailable() { + updateEnvironment(); + + if (qEnvironmentVariableIsEmpty("OPCUA_HOST") && qEnvironmentVariableIsEmpty("OPCUA_PORT")) { + m_testServerPath = qApp->applicationDirPath() + +#if defined(Q_OS_MACOS) + + QLatin1String("/../../open62541-testserver/open62541-testserver.app/Contents/MacOS/open62541-testserver") +#else + +#ifdef Q_OS_WIN + + QLatin1String("/..") +#endif + + QLatin1String("/../../open62541-testserver/open62541-testserver") +#ifdef Q_OS_WIN + + QLatin1String(".exe") +#endif + +#endif + ; + qDebug() << "Server Path:" << m_testServerPath; + if (!QFile::exists(m_testServerPath)) { + qFatal("all auto tests rely on an open62541-based test-server"); + } + + // In this case the test is supposed to open its own server. + // Unfortunately there is no way to check if the server has started up successfully + // because of missing error handling. + // This checks will detect other servers blocking the port. + + // Check for running server + QTcpSocket socket; + socket.connectToHost(defaultHost, defaultPort); + QVERIFY2(socket.waitForConnected(1500) == false, "Server is already running"); + + // Check for running server which does not respond + QTcpServer server; + QVERIFY2(server.listen(defaultHost, defaultPort) == true, "Port is occupied by another process. Check for defunct server."); + server.close(); + + qDebug() << "Starting test server"; + //m_serverProcess.setProcessChannelMode(QProcess::ForwardedChannels); + m_serverProcess.start(m_testServerPath); + QVERIFY2(m_serverProcess.waitForStarted(), qPrintable(m_serverProcess.errorString())); + // Let the server come up + QTest::qSleep(2000); + } + const QString host = envOrDefault("OPCUA_HOST", defaultHost.toString()); + const QString port = envOrDefault("OPCUA_PORT", QString::number(defaultPort)); + m_opcuaDiscoveryUrl = QString::fromLatin1("opc.tcp://%1:%2").arg(host).arg(port); + } + void qmlEngineAvailable(QQmlEngine *engine) { + bool value = false; +#ifdef SERVER_SUPPORTS_SECURITY + value = true; +#endif + engine->rootContext()->setContextProperty("SERVER_SUPPORTS_SECURITY", value); + engine->rootContext()->setContextProperty("OPCUA_DISCOVERY_URL", m_opcuaDiscoveryUrl); + qmlRegisterType("App", 1, 0, "MyClass"); + } + void cleanupTestCase() { + if (m_serverProcess.state() == QProcess::Running) { + m_serverProcess.kill(); + m_serverProcess.waitForFinished(2000); + } + } +private: + QProcess m_serverProcess; + QString m_testServerPath; + QString m_opcuaDiscoveryUrl; +}; + +QUICK_TEST_MAIN_WITH_SETUP(opcua, SetupClass) + +#include "tst_clientSetupInCpp.moc" diff --git a/tests/auto/clientSetupInCpp/tst_clientSetupInCpp.qml b/tests/auto/clientSetupInCpp/tst_clientSetupInCpp.qml new file mode 100644 index 0000000..afbb8ba --- /dev/null +++ b/tests/auto/clientSetupInCpp/tst_clientSetupInCpp.qml @@ -0,0 +1,82 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt OPC UA module. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.3 +import QtTest 1.0 +import QtOpcUa 5.13 as QtOpcUa +import App 1.0 + +Item { + QtOpcUa.Connection { + id: connection + connection: myclass.connection + } + + MyClass { + id: myclass + } + + Component.onCompleted: myclass.startConnection() + + TestCase { + name: "Create String Node Id" + when: node1.readyToUse + + function test_nodeTest() { + compare(node1.value, "Value", ""); + compare(node1.browseName, "theStringId"); + compare(node1.nodeClass, QtOpcUa.Constants.NodeClass.Variable); + compare(node1.displayName.text, "theStringId"); + compare(node1.description.text, "Description for ns=3;s=theStringId"); + compare(node1.status, QtOpcUa.Node.Status.Valid); + tryCompare(node1, "monitored", true); + compare(node1.publishingInterval, 100.0); + + compare(connection.currentEndpoint.endpointUrl, "opc.tcp://127.0.0.1:43344"); + compare(connection.currentEndpoint.securityPolicy, "http://opcfoundation.org/UA/SecurityPolicy#None"); + compare(connection.currentEndpoint.server.applicationUri, "urn:unconfigured:application"); + } + + QtOpcUa.ValueNode { + connection: connection + nodeId: QtOpcUa.NodeId { + ns: "Test Namespace" + identifier: "s=theStringId" + } + id: node1 + } + } +} -- cgit v1.2.3