Qt OPC UA Viewer Example
/**************************************************************************** ** ** Copyright (C) 2018 The Qt Company Ltd. ** Contact: http://www.qt.io/licensing/ ** ** This file is part of the examples of the Qt OPC UA module. ** ** $QT_BEGIN_LICENSE:BSD$ ** 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 https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** BSD License Usage ** Alternatively, you may use this file under the terms of the BSD license ** as follows: ** ** "Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions are ** met: ** * Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** * Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in ** the documentation and/or other materials provided with the ** distribution. ** * Neither the name of The Qt Company Ltd nor the names of its ** contributors may be used to endorse or promote products derived ** from this software without specific prior written permission. ** ** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ** ** $QT_END_LICENSE$ ** ****************************************************************************/#include "mainwindow.h" #include "opcuamodel.h" #include "certificatedialog.h" #include "ui_mainwindow.h" #include <QApplication> #include <QDir> #include <QMessageBox> #include <QTextCharFormat> #include <QTextBlock> #include <QOpcUaProvider> #include <QOpcUaAuthenticationInformation> #include <QOpcUaErrorState> static MainWindow *mainWindowGlobal = nullptr; static QtMessageHandler oldMessageHandler = nullptr; static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) { if (!mainWindowGlobal) return; QString message; QColor color = Qt::black; switch (type) { case QtWarningMsg: message = QObject::tr("Warning"); color = Qt::darkYellow; break; case QtCriticalMsg: message = QObject::tr("Critical"); color = Qt::darkRed; break; case QtFatalMsg: message = QObject::tr("Fatal"); color = Qt::darkRed; break; case QtInfoMsg: message = QObject::tr("Info"); break; case QtDebugMsg: message = QObject::tr("Debug"); break; } message += QLatin1String(": "); message += msg; const QString contextStr = QStringLiteral(" (%1:%2, %3)").arg(context.file).arg(context.line).arg(context.function); // Logging messages from backends are sent from different threads and need to be // synchronized with the GUI thread. QMetaObject::invokeMethod(mainWindowGlobal, "log", Qt::QueuedConnection, Q_ARG(QString, message), Q_ARG(QString, contextStr), Q_ARG(QColor, color)); if (oldMessageHandler) oldMessageHandler(type, context, msg); } MainWindow::MainWindow(const QString &initialUrl, QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) , mOpcUaModel(new OpcUaModel(this)) , mOpcUaProvider(new QOpcUaProvider(this)) { ui->setupUi(this); ui->host->setText(initialUrl); mainWindowGlobal = this; connect(ui->quitAction, &QAction::triggered, this, &QWidget::close); ui->quitAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_Q)); connect(ui->aboutAction, &QAction::triggered, this, &QApplication::aboutQt); ui->aboutAction->setShortcut(QKeySequence(QKeySequence::HelpContents)); updateUiState(); ui->opcUaPlugin->addItems(QOpcUaProvider::availableBackends()); ui->treeView->setModel(mOpcUaModel); ui->treeView->header()->setSectionResizeMode(QHeaderView::ResizeToContents); if (ui->opcUaPlugin->count() == 0) { ui->opcUaPlugin->setDisabled(true); ui->connectButton->setDisabled(true); QMessageBox::critical(this, tr("No OPCUA plugins available"), tr("The list of available OPCUA plugins is empty. No connection possible.")); } mContextMenu = new QMenu(ui->treeView); mContextMenuAction = mContextMenu->addAction(tr("Enable Monitoring"), this, &MainWindow::toggleMonitoring); ui->treeView->setContextMenuPolicy(Qt::CustomContextMenu); connect(ui->treeView, &QTreeView::customContextMenuRequested, this, &MainWindow::openCustomContextMenu); connect(ui->findServersButton, &QPushButton::clicked, this, &MainWindow::findServers); connect(ui->host, &QLineEdit::returnPressed, this->ui->findServersButton, [this]() { this->ui->findServersButton->animateClick(); }); connect(ui->getEndpointsButton, &QPushButton::clicked, this, &MainWindow::getEndpoints); connect(ui->connectButton, &QPushButton::clicked, this, &MainWindow::connectToServer); oldMessageHandler = qInstallMessageHandler(&messageHandler); setupPkiConfiguration(); m_identity = m_pkiConfig.applicationIdentity(); } MainWindow::~MainWindow() { delete ui; } void MainWindow::setupPkiConfiguration() { QString pkidir = QCoreApplication::applicationDirPath(); #ifdef Q_OS_WIN pkidir += "../"; #endif pkidir += "/pki"; m_pkiConfig.setClientCertificateFile(pkidir + "/own/certs/opcuaviewer.der"); m_pkiConfig.setPrivateKeyFile(pkidir + "/own/private/opcuaviewer.pem"); m_pkiConfig.setTrustListDirectory(pkidir + "/trusted/certs"); m_pkiConfig.setRevocationListDirectory(pkidir + "/trusted/crl"); m_pkiConfig.setIssuerListDirectory(pkidir + "/issuers/certs"); m_pkiConfig.setIssuerRevocationListDirectory(pkidir + "/issuers/crl"); // create the folders if they don't exist yet createPkiFolders(); } void MainWindow::createClient() { if (mOpcUaClient == nullptr) { mOpcUaClient = mOpcUaProvider->createClient(ui->opcUaPlugin->currentText()); if (!mOpcUaClient) { const QString message(tr("Connecting to the given sever failed. See the log for details.")); log(message, QString(), Qt::red); QMessageBox::critical(this, tr("Failed to connect to server"), message); return; } connect(mOpcUaClient, &QOpcUaClient::connectError, this, &MainWindow::showErrorDialog); mOpcUaClient->setApplicationIdentity(m_identity); mOpcUaClient->setPkiConfiguration(m_pkiConfig); if (mOpcUaClient->supportedUserTokenTypes().contains(QOpcUaUserTokenPolicy::TokenType::Certificate)) { QOpcUaAuthenticationInformation authInfo; authInfo.setCertificateAuthentication(); mOpcUaClient->setAuthenticationInformation(authInfo); } connect(mOpcUaClient, &QOpcUaClient::connected, this, &MainWindow::clientConnected); connect(mOpcUaClient, &QOpcUaClient::disconnected, this, &MainWindow::clientDisconnected); connect(mOpcUaClient, &QOpcUaClient::errorChanged, this, &MainWindow::clientError); connect(mOpcUaClient, &QOpcUaClient::stateChanged, this, &MainWindow::clientState); connect(mOpcUaClient, &QOpcUaClient::endpointsRequestFinished, this, &MainWindow::getEndpointsComplete); connect(mOpcUaClient, &QOpcUaClient::findServersFinished, this, &MainWindow::findServersComplete); } } void MainWindow::findServers() { QStringList localeIds; QStringList serverUris; QUrl url(ui->host->text()); updateUiState(); createClient(); // set default port if missing if (url.port() == -1) url.setPort(4840); if (mOpcUaClient) { mOpcUaClient->findServers(url, localeIds, serverUris); qDebug() << "Discovering servers on " << url.toString(); } } void MainWindow::findServersComplete(const QList<QOpcUaApplicationDescription> &servers, QOpcUa::UaStatusCode statusCode) { QOpcUaApplicationDescription server; if (isSuccessStatus(statusCode)) { ui->servers->clear(); for (const auto &server : servers) { const auto urls = server.discoveryUrls(); for (const auto &url : qAsConst(urls)) ui->servers->addItem(url); } } updateUiState(); } void MainWindow::getEndpoints() { ui->endpoints->clear(); updateUiState(); if (ui->servers->currentIndex() >= 0) { const QString serverUrl = ui->servers->currentText(); createClient(); mOpcUaClient->requestEndpoints(serverUrl); } } void MainWindow::getEndpointsComplete(const QList<QOpcUaEndpointDescription> &endpoints, QOpcUa::UaStatusCode statusCode) { int index = 0; const std::array<const char *, 4> modes = { "Invalid", "None", "Sign", "SignAndEncrypt" }; if (isSuccessStatus(statusCode)) { mEndpointList = endpoints; for (const auto &endpoint : endpoints) { if (endpoint.securityMode() >= modes.size()) { qWarning() << "Invalid security mode"; continue; } const QString EndpointName = QString("%1 (%2)") .arg(endpoint.securityPolicy(), modes[endpoint.securityMode()]); ui->endpoints->addItem(EndpointName, index++); } } updateUiState(); } void MainWindow::connectToServer() { if (mClientConnected) { mOpcUaClient->disconnectFromEndpoint(); return; } if (ui->endpoints->currentIndex() >= 0) { m_endpoint = mEndpointList[ui->endpoints->currentIndex()]; createClient(); mOpcUaClient->connectToEndpoint(m_endpoint); } } void MainWindow::clientConnected() { mClientConnected = true; updateUiState(); connect(mOpcUaClient, &QOpcUaClient::namespaceArrayUpdated, this, &MainWindow::namespacesArrayUpdated); mOpcUaClient->updateNamespaceArray(); } void MainWindow::clientDisconnected() { mClientConnected = false; mOpcUaClient->deleteLater(); mOpcUaClient = nullptr; mOpcUaModel->setOpcUaClient(nullptr); updateUiState(); } void MainWindow::namespacesArrayUpdated(const QStringList &namespaceArray) { if (namespaceArray.isEmpty()) { qWarning() << "Failed to retrieve the namespaces array"; return; } disconnect(mOpcUaClient, &QOpcUaClient::namespaceArrayUpdated, this, &MainWindow::namespacesArrayUpdated); mOpcUaModel->setOpcUaClient(mOpcUaClient); ui->treeView->header()->setSectionResizeMode(1 /* Value column*/, QHeaderView::Interactive); } void MainWindow::clientError(QOpcUaClient::ClientError error) { qDebug() << "Client error changed" << error; } void MainWindow::clientState(QOpcUaClient::ClientState state) { qDebug() << "Client state changed" << state; } void MainWindow::updateUiState() { // allow changing the backend only if it was not already created ui->opcUaPlugin->setEnabled(mOpcUaClient == nullptr); ui->connectButton->setText(mClientConnected ? tr("Disconnect") : tr("Connect")); if (mClientConnected) { ui->host->setEnabled(false); ui->servers->setEnabled(false); ui->endpoints->setEnabled(false); ui->findServersButton->setEnabled(false); ui->getEndpointsButton->setEnabled(false); ui->connectButton->setEnabled(true); } else { ui->host->setEnabled(true); ui->servers->setEnabled(ui->servers->count() > 0); ui->endpoints->setEnabled(ui->endpoints->count() > 0); ui->findServersButton->setDisabled(ui->host->text().isEmpty()); ui->getEndpointsButton->setEnabled(ui->servers->currentIndex() != -1); ui->connectButton->setEnabled(ui->endpoints->currentIndex() != -1); } if (!mOpcUaClient) { ui->servers->setEnabled(false); ui->endpoints->setEnabled(false); ui->getEndpointsButton->setEnabled(false); ui->connectButton->setEnabled(false); } } void MainWindow::log(const QString &text, const QString &context, const QColor &color) { auto cf = ui->log->currentCharFormat(); cf.setForeground(color); ui->log->setCurrentCharFormat(cf); ui->log->appendPlainText(text); if (!context.isEmpty()) { cf.setForeground(Qt::gray); ui->log->setCurrentCharFormat(cf); ui->log->insertPlainText(context); } } void MainWindow::log(const QString &text, const QColor &color) { log(text, QString(), color); } bool MainWindow::createPkiPath(const QString &path) { const QString msg = tr("Creating PKI path '%1': %2"); QDir dir; const bool ret = dir.mkpath(path); if (ret) qDebug() << msg.arg(path, "SUCCESS."); else qCritical("%s", qPrintable(msg.arg(path, "FAILED."))); return ret; } bool MainWindow::createPkiFolders() { bool result = createPkiPath(m_pkiConfig.trustListDirectory()); if (!result) return result; result = createPkiPath(m_pkiConfig.revocationListDirectory()); if (!result) return result; result = createPkiPath(m_pkiConfig.issuerListDirectory()); if (!result) return result; result = createPkiPath(m_pkiConfig.issuerRevocationListDirectory()); if (!result) return result; return result; } void MainWindow::showErrorDialog(QOpcUaErrorState *errorState) { int result = 0; const QString statuscode = QOpcUa::statusToString(errorState->errorCode()); QString msg = errorState->isClientSideError() ? tr("The client reported: ") : tr("The server reported: "); switch (errorState->connectionStep()) { case QOpcUaErrorState::ConnectionStep::Unknown: break; case QOpcUaErrorState::ConnectionStep::CertificateValidation: { CertificateDialog dlg(this); msg += tr("Server certificate validation failed with error 0x%1 (%2).\nClick 'Abort' to abort the connect, or 'Ignore' to continue connecting.") .arg(static_cast<ulong>(errorState->errorCode()), 8, 16, QLatin1Char('0')).arg(statuscode); result = dlg.showCertificate(msg, m_endpoint.serverCertificate(), m_pkiConfig.trustListDirectory()); errorState->setIgnoreError(result == 1); } break; case QOpcUaErrorState::ConnectionStep::OpenSecureChannel: msg += tr("OpenSecureChannel failed with error 0x%1 (%2).").arg(errorState->errorCode(), 8, 16, QLatin1Char('0')).arg(statuscode); QMessageBox::warning(this, tr("Connection Error"), msg); break; case QOpcUaErrorState::ConnectionStep::CreateSession: msg += tr("CreateSession failed with error 0x%1 (%2).").arg(errorState->errorCode(), 8, 16, QLatin1Char('0')).arg(statuscode); QMessageBox::warning(this, tr("Connection Error"), msg); break; case QOpcUaErrorState::ConnectionStep::ActivateSession: msg += tr("ActivateSession failed with error 0x%1 (%2).").arg(errorState->errorCode(), 8, 16, QLatin1Char('0')).arg(statuscode); QMessageBox::warning(this, tr("Connection Error"), msg); break; } } void MainWindow::openCustomContextMenu(const QPoint &point) { QModelIndex index = ui->treeView->indexAt(point); // show the context menu only for the value column if (index.isValid() && index.column() == 1) { TreeItem* item = static_cast<TreeItem *>(index.internalPointer()); if (item) { mContextMenuAction->setData(index); mContextMenuAction->setEnabled(item->supportsMonitoring()); mContextMenuAction->setText(item->monitoringEnabled() ? tr("Disable Monitoring") : tr("Enable Monitoring")); mContextMenu->exec(ui->treeView->viewport()->mapToGlobal(point)); } } } void MainWindow::toggleMonitoring() { QModelIndex index = mContextMenuAction->data().toModelIndex(); if (index.isValid()) { TreeItem* item = static_cast<TreeItem *>(index.internalPointer()); if (item) { item->setMonitoringEnabled(!item->monitoringEnabled()); } } }