Qt OPC UA Viewer
// Copyright (C) 2018 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause #include "mainwindow.h" #include "ui_mainwindow.h" #include "certificatedialog.h" #include "opcuamodel.h" #include "treeitem.h" #include <QOpcUaAuthenticationInformation> #include <QOpcUaErrorState> #include <QOpcUaGenericStructHandler> #include <QOpcUaHistoryReadResponse> #include <QOpcUaProvider> #include <QApplication> #include <QDir> #include <QMessageBox> #include <QStandardPaths> using namespace Qt::Literals::StringLiterals; 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 += ": "_L1; message += msg; const QString contextStr = u" (%1:%2, %3)"_s.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) { 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); mContextMenuMonitoringAction = mContextMenu->addAction(tr("Enable Monitoring"), this, &MainWindow::toggleMonitoring); mContextMenuHistorizingAction = mContextMenu->addAction(tr("Request historic data"), this, &MainWindow::showHistorizing); 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; } static bool copyDirRecursively(const QString &from, const QString &to) { const QDir srcDir(from); const QDir targetDir(to); if (!QDir().mkpath(to)) return false; const QFileInfoList infos = srcDir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot); for (const QFileInfo &info : infos) { const QString srcItemPath = info.absoluteFilePath(); const QString dstItemPath = targetDir.absoluteFilePath(info.fileName()); if (info.isDir()) { if (!copyDirRecursively(srcItemPath, dstItemPath)) return false; } else if (info.isFile()) { if (!QFile::copy(srcItemPath, dstItemPath)) return false; } } return true; } void MainWindow::setupPkiConfiguration() { const QDir pkidir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/pki"); if (!pkidir.exists() && !copyDirRecursively(":/pki", pkidir.path())) qFatal("Could not set up directory %s!", qUtf8Printable(pkidir.path())); m_pkiConfig.setClientCertificateFile(pkidir.absoluteFilePath("own/certs/opcuaviewer.der")); m_pkiConfig.setPrivateKeyFile(pkidir.absoluteFilePath("own/private/opcuaviewer.pem")); m_pkiConfig.setTrustListDirectory(pkidir.absoluteFilePath("trusted/certs")); m_pkiConfig.setRevocationListDirectory(pkidir.absoluteFilePath("trusted/crl")); m_pkiConfig.setIssuerListDirectory(pkidir.absoluteFilePath("issuers/certs")); m_pkiConfig.setIssuerRevocationListDirectory(pkidir.absoluteFilePath("issuers/crl")); const QStringList toCreate = { m_pkiConfig.issuerListDirectory(), m_pkiConfig.issuerRevocationListDirectory() }; for (const QString &dir : toCreate) { if (!QDir().mkpath(dir)) qFatal("Could not create directory %s!", qUtf8Printable(dir)); } } 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) { if (isSuccessStatus(statusCode)) { ui->servers->clear(); for (const auto &server : servers) { const auto urls = server.discoveryUrls(); for (const auto &url : std::as_const(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) { if (isSuccessStatus(statusCode)) { mEndpointList = endpoints; int index = 0; for (const auto &endpoint : endpoints) { const QString mode = QVariant::fromValue(endpoint.securityMode()).toString(); const QString endpointName = u"%1 (%2)"_s.arg(endpoint.securityPolicy(), mode); 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); mOpcUaModel->setGenericStructHandler(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); mGenericStructHandler.reset(new QOpcUaGenericStructHandler(mOpcUaClient)); connect(mGenericStructHandler.get(), &QOpcUaGenericStructHandler::initializedChanged, this, &MainWindow::handleGenericStructHandlerInitFinished); mGenericStructHandler->initialize(); } void MainWindow::handleGenericStructHandlerInitFinished(bool success) { if (!success) { qWarning() << "Failed to initialize generic struct handler, decoding of generic structs will be unavailable"; } else { mOpcUaModel->setGenericStructHandler(mGenericStructHandler.get()); } 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); } 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, '0'_L1).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, '0'_L1).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, '0'_L1).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, '0'_L1).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) { mContextMenuMonitoringAction->setData(index); mContextMenuMonitoringAction->setEnabled(item->supportsMonitoring()); mContextMenuMonitoringAction->setText(item->monitoringEnabled() ? tr("Disable Monitoring") : tr("Enable Monitoring")); mContextMenuHistorizingAction->setData(index); QModelIndex isHistoricIndex = mOpcUaModel->index(index.row(), 7, index.parent()); mContextMenuHistorizingAction->setEnabled(mOpcUaModel->data(isHistoricIndex, Qt::DisplayRole).toString() == "true"); mContextMenu->exec(ui->treeView->viewport()->mapToGlobal(point)); } } } void MainWindow::toggleMonitoring() { QModelIndex index = mContextMenuMonitoringAction->data().toModelIndex(); if (index.isValid()) { TreeItem* item = static_cast<TreeItem *>(index.internalPointer()); if (item) { item->setMonitoringEnabled(!item->monitoringEnabled()); } } } void MainWindow::showHistorizing() { QModelIndex modelIndex = mContextMenuHistorizingAction->data().toModelIndex(); QModelIndex nodeIdIndex = mOpcUaModel->index(modelIndex.row(), 4, modelIndex.parent()); QString nodeId = mOpcUaModel->data(nodeIdIndex, Qt::DisplayRole).toString(); auto request = QOpcUaHistoryReadRawRequest( {QOpcUaReadItem(nodeId)}, QDateTime::currentDateTime(), QDateTime::currentDateTime().addDays(-2), 5, false ); mHistoryReadResponse.reset(mOpcUaClient->readHistoryData(request)); if (mHistoryReadResponse) { QObject::connect(mHistoryReadResponse.get(), &QOpcUaHistoryReadResponse::readHistoryDataFinished, this, &MainWindow::handleReadHistoryDataFinished); QObject::connect(mHistoryReadResponse.get(), &QOpcUaHistoryReadResponse::stateChanged, this, [](QOpcUaHistoryReadResponse::State state) { qDebug() << "History read state changed to" << state; }); } else { qWarning() << "Failed to request history data"; } } void MainWindow::handleReadHistoryDataFinished(QList<QOpcUaHistoryData> results, QOpcUa::UaStatusCode serviceResult) { if (serviceResult != QOpcUa::UaStatusCode::Good) { qWarning() << "readHistoryData request finished with bad status code: " << serviceResult; return; } for (int i = 0; i < results.count(); ++i) { qInfo() << "NodeId:" << results.at(i).nodeId() << "; statusCode:" << results.at(i).statusCode() << "; returned values:" << results.at(i).count(); for (int j = 0; j < results.at(i).count(); ++j) { qInfo() << j << "source timestamp:" << results.at(i).result()[j].sourceTimestamp() << "server timestamp:" << results.at(i).result()[j].serverTimestamp() << "value:" << results.at(i).result()[j].value(); } } }