Qt OPC UA Viewer▲
Sélectionnez
// 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 &
amp;context, const
QString &
amp;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 &
amp;initialUrl, QWidget *
parent) : QMainWindow(parent)
, ui(new
Ui::
MainWindow)
, mOpcUaModel(new
OpcUaModel(this
))
, mOpcUaProvider(new
QOpcUaProvider(this
))
{
ui-&
gt;setupUi(this
);
ui-&
gt;host-&
gt;setText(initialUrl);
mainWindowGlobal =
this
;
connect(ui-&
gt;quitAction, &
amp;QAction::
triggered, this
, &
amp;QWidget::
close);
ui-&
gt;quitAction-&
gt;setShortcut(QKeySequence(Qt::
CTRL |
Qt::
Key_Q));
connect(ui-&
gt;aboutAction, &
amp;QAction::
triggered, this
, &
amp;QApplication::
aboutQt);
ui-&
gt;aboutAction-&
gt;setShortcut(QKeySequence(QKeySequence::
HelpContents));
updateUiState();
ui-&
gt;opcUaPlugin-&
gt;addItems(QOpcUaProvider::
availableBackends());
ui-&
gt;treeView-&
gt;setModel(mOpcUaModel);
ui-&
gt;treeView-&
gt;header()-&
gt;setSectionResizeMode(QHeaderView::
ResizeToContents);
if
(ui-&
gt;opcUaPlugin-&
gt;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-&
gt;treeView);
mContextMenuMonitoringAction =
mContextMenu-&
gt;addAction(tr("Enable Monitoring"
), this
, &
amp;MainWindow::
toggleMonitoring);
mContextMenuHistorizingAction =
mContextMenu-&
gt;addAction(tr("Request historic data"
), this
, &
amp;MainWindow::
showHistorizing);
ui-&
gt;treeView-&
gt;setContextMenuPolicy(Qt::
CustomContextMenu);
connect(ui-&
gt;treeView, &
amp;QTreeView::
customContextMenuRequested, this
, &
amp;MainWindow::
openCustomContextMenu);
connect(ui-&
gt;findServersButton, &
amp;QPushButton::
clicked, this
, &
amp;MainWindow::
findServers);
connect(ui-&
gt;host, &
amp;QLineEdit::
returnPressed, this
-&
gt;ui-&
gt;findServersButton,
[this
]() {
this
-&
gt;ui-&
gt;findServersButton-&
gt;animateClick(); }
);
connect(ui-&
gt;getEndpointsButton, &
amp;QPushButton::
clicked, this
, &
amp;MainWindow::
getEndpoints);
connect(ui-&
gt;connectButton, &
amp;QPushButton::
clicked, this
, &
amp;MainWindow::
connectToServer);
oldMessageHandler =
qInstallMessageHandler(&
amp;messageHandler);
setupPkiConfiguration();
m_identity =
m_pkiConfig.applicationIdentity();
}
MainWindow::
~
MainWindow()
{
delete
ui;
}
static
bool
copyDirRecursively(const
QString &
amp;from, const
QString &
amp;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 &
amp;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() &
amp;&
amp; !
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 &
amp;dir : toCreate) {
if
(!
QDir().mkpath(dir))
qFatal("Could not create directory %s!"
, qUtf8Printable(dir));
}
}
void
MainWindow::
createClient()
{
if
(mOpcUaClient ==
nullptr
) {
mOpcUaClient =
mOpcUaProvider-&
gt;createClient(ui-&
gt;opcUaPlugin-&
gt;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, &
amp;QOpcUaClient::
connectError, this
, &
amp;MainWindow::
showErrorDialog);
mOpcUaClient-&
gt;setApplicationIdentity(m_identity);
mOpcUaClient-&
gt;setPkiConfiguration(m_pkiConfig);
if
(mOpcUaClient-&
gt;supportedUserTokenTypes().contains(QOpcUaUserTokenPolicy::TokenType::
Certificate)) {
QOpcUaAuthenticationInformation authInfo;
authInfo.setCertificateAuthentication();
mOpcUaClient-&
gt;setAuthenticationInformation(authInfo);
}
connect(mOpcUaClient, &
amp;QOpcUaClient::
connected, this
, &
amp;MainWindow::
clientConnected);
connect(mOpcUaClient, &
amp;QOpcUaClient::
disconnected, this
, &
amp;MainWindow::
clientDisconnected);
connect(mOpcUaClient, &
amp;QOpcUaClient::
errorChanged, this
, &
amp;MainWindow::
clientError);
connect(mOpcUaClient, &
amp;QOpcUaClient::
stateChanged, this
, &
amp;MainWindow::
clientState);
connect(mOpcUaClient, &
amp;QOpcUaClient::
endpointsRequestFinished, this
, &
amp;MainWindow::
getEndpointsComplete);
connect(mOpcUaClient, &
amp;QOpcUaClient::
findServersFinished, this
, &
amp;MainWindow::
findServersComplete);
}
}
void
MainWindow::
findServers()
{
QStringList localeIds;
QStringList serverUris;
QUrl url(ui-&
gt;host-&
gt;text());
updateUiState();
createClient();
// set default port if missing
if
(url.port() ==
-
1
) url.setPort(4840
);
if
(mOpcUaClient) {
mOpcUaClient-&
gt;findServers(url, localeIds, serverUris);
qDebug() &
lt;&
lt; "Discovering servers on "
&
lt;&
lt; url.toString();
}
}
void
MainWindow::
findServersComplete(const
QList&
lt;QOpcUaApplicationDescription&
gt; &
amp;servers, QOpcUa::
UaStatusCode statusCode)
{
if
(isSuccessStatus(statusCode)) {
ui-&
gt;servers-&
gt;clear();
for
(const
auto
&
amp;server : servers) {
const
auto
urls =
server.discoveryUrls();
for
(const
auto
&
amp;url : std::
as_const(urls))
ui-&
gt;servers-&
gt;addItem(url);
}
}
updateUiState();
}
void
MainWindow::
getEndpoints()
{
ui-&
gt;endpoints-&
gt;clear();
updateUiState();
if
(ui-&
gt;servers-&
gt;currentIndex() &
gt;=
0
) {
const
QString serverUrl =
ui-&
gt;servers-&
gt;currentText();
createClient();
mOpcUaClient-&
gt;requestEndpoints(serverUrl);
}
}
void
MainWindow::
getEndpointsComplete(const
QList&
lt;QOpcUaEndpointDescription&
gt; &
amp;endpoints, QOpcUa::
UaStatusCode statusCode)
{
if
(isSuccessStatus(statusCode)) {
mEndpointList =
endpoints;
int
index =
0
;
for
(const
auto
&
amp;endpoint : endpoints) {
const
QString mode =
QVariant::
fromValue(endpoint.securityMode()).toString();
const
QString endpointName =
u"%1 (%2)"
_s.arg(endpoint.securityPolicy(), mode);
ui-&
gt;endpoints-&
gt;addItem(endpointName, index++
);
}
}
updateUiState();
}
void
MainWindow::
connectToServer()
{
if
(mClientConnected) {
mOpcUaClient-&
gt;disconnectFromEndpoint();
return
;
}
if
(ui-&
gt;endpoints-&
gt;currentIndex() &
gt;=
0
) {
m_endpoint =
mEndpointList[ui-&
gt;endpoints-&
gt;currentIndex()];
createClient();
mOpcUaClient-&
gt;connectToEndpoint(m_endpoint);
}
}
void
MainWindow::
clientConnected()
{
mClientConnected =
true
;
updateUiState();
connect(mOpcUaClient, &
amp;QOpcUaClient::
namespaceArrayUpdated, this
, &
amp;MainWindow::
namespacesArrayUpdated);
mOpcUaClient-&
gt;updateNamespaceArray();
}
void
MainWindow::
clientDisconnected()
{
mClientConnected =
false
;
mOpcUaClient-&
gt;deleteLater();
mOpcUaClient =
nullptr
;
mOpcUaModel-&
gt;setOpcUaClient(nullptr
);
mOpcUaModel-&
gt;setGenericStructHandler(nullptr
);
updateUiState();
}
void
MainWindow::
namespacesArrayUpdated(const
QStringList &
amp;namespaceArray)
{
if
(namespaceArray.isEmpty()) {
qWarning() &
lt;&
lt; "Failed to retrieve the namespaces array"
;
return
;
}
disconnect(mOpcUaClient, &
amp;QOpcUaClient::
namespaceArrayUpdated, this
, &
amp;MainWindow::
namespacesArrayUpdated);
mGenericStructHandler.reset(new
QOpcUaGenericStructHandler(mOpcUaClient));
connect(mGenericStructHandler.get(), &
amp;QOpcUaGenericStructHandler::
initializedChanged, this
, &
amp;MainWindow::
handleGenericStructHandlerInitFinished);
mGenericStructHandler-&
gt;initialize();
}
void
MainWindow::
handleGenericStructHandlerInitFinished(bool
success)
{
if
(!
success) {
qWarning() &
lt;&
lt; "Failed to initialize generic struct handler, decoding of generic structs will be unavailable"
;
}
else
{
mOpcUaModel-&
gt;setGenericStructHandler(mGenericStructHandler.get());
}
mOpcUaModel-&
gt;setOpcUaClient(mOpcUaClient);
ui-&
gt;treeView-&
gt;header()-&
gt;setSectionResizeMode(1
/* Value column*/
, QHeaderView::
Interactive);
}
void
MainWindow::
clientError(QOpcUaClient::
ClientError error)
{
qDebug() &
lt;&
lt; "Client error changed"
&
lt;&
lt; error;
}
void
MainWindow::
clientState(QOpcUaClient::
ClientState state)
{
qDebug() &
lt;&
lt; "Client state changed"
&
lt;&
lt; state;
}
void
MainWindow::
updateUiState()
{
// allow changing the backend only if it was not already created
ui-&
gt;opcUaPlugin-&
gt;setEnabled(mOpcUaClient ==
nullptr
);
ui-&
gt;connectButton-&
gt;setText(mClientConnected ? tr("Disconnect"
) : tr("Connect"
));
if
(mClientConnected) {
ui-&
gt;host-&
gt;setEnabled(false
);
ui-&
gt;servers-&
gt;setEnabled(false
);
ui-&
gt;endpoints-&
gt;setEnabled(false
);
ui-&
gt;findServersButton-&
gt;setEnabled(false
);
ui-&
gt;getEndpointsButton-&
gt;setEnabled(false
);
ui-&
gt;connectButton-&
gt;setEnabled(true
);
}
else
{
ui-&
gt;host-&
gt;setEnabled(true
);
ui-&
gt;servers-&
gt;setEnabled(ui-&
gt;servers-&
gt;count() &
gt; 0
);
ui-&
gt;endpoints-&
gt;setEnabled(ui-&
gt;endpoints-&
gt;count() &
gt; 0
);
ui-&
gt;findServersButton-&
gt;setDisabled(ui-&
gt;host-&
gt;text().isEmpty());
ui-&
gt;getEndpointsButton-&
gt;setEnabled(ui-&
gt;servers-&
gt;currentIndex() !=
-
1
);
ui-&
gt;connectButton-&
gt;setEnabled(ui-&
gt;endpoints-&
gt;currentIndex() !=
-
1
);
}
if
(!
mOpcUaClient) {
ui-&
gt;servers-&
gt;setEnabled(false
);
ui-&
gt;endpoints-&
gt;setEnabled(false
);
ui-&
gt;getEndpointsButton-&
gt;setEnabled(false
);
ui-&
gt;connectButton-&
gt;setEnabled(false
);
}
}
void
MainWindow::
log(const
QString &
amp;text, const
QString &
amp;context, const
QColor &
amp;color)
{
auto
cf =
ui-&
gt;log-&
gt;currentCharFormat();
cf.setForeground(color);
ui-&
gt;log-&
gt;setCurrentCharFormat(cf);
ui-&
gt;log-&
gt;appendPlainText(text);
if
(!
context.isEmpty()) {
cf.setForeground(Qt::
gray);
ui-&
gt;log-&
gt;setCurrentCharFormat(cf);
ui-&
gt;log-&
gt;insertPlainText(context);
}
}
void
MainWindow::
log(const
QString &
amp;text, const
QColor &
amp;color)
{
log(text, QString(), color);
}
void
MainWindow::
showErrorDialog(QOpcUaErrorState *
errorState)
{
int
result =
0
;
const
QString statuscode =
QOpcUa::
statusToString(errorState-&
gt;errorCode());
QString msg =
errorState-&
gt;isClientSideError() ? tr("The client reported: "
) : tr("The server reported: "
);
switch
(errorState-&
gt;connectionStep()) {
case
QOpcUaErrorState::ConnectionStep::
Unknown:
break
;
case
QOpcUaErrorState::ConnectionStep::
CertificateValidation: {
CertificateDialog dlg(this
);
msg +=
tr("Server certificate validation failed with error 0x%1 (%2).
\n
Click 'Abort' to abort the connect, or 'Ignore' to continue connecting."
)
.arg(static_cast
&
lt;ulong&
gt;(errorState-&
gt;errorCode()), 8
, 16
, '0'
_L1).arg(statuscode);
result =
dlg.showCertificate(msg, m_endpoint.serverCertificate(), m_pkiConfig.trustListDirectory());
errorState-&
gt;setIgnoreError(result ==
1
);
}
break
;
case
QOpcUaErrorState::ConnectionStep::
OpenSecureChannel:
msg +=
tr("OpenSecureChannel failed with error 0x%1 (%2)."
).arg(errorState-&
gt;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-&
gt;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-&
gt;errorCode(), 8
, 16
, '0'
_L1).arg(statuscode);
QMessageBox::
warning(this
, tr("Connection Error"
), msg);
break
;
}
}
void
MainWindow::
openCustomContextMenu(const
QPoint &
amp;point)
{
QModelIndex index =
ui-&
gt;treeView-&
gt;indexAt(point);
// show the context menu only for the value column
if
(index.isValid() &
amp;&
amp; index.column() ==
1
) {
TreeItem*
item =
static_cast
&
lt;TreeItem *&
gt;(index.internalPointer());
if
(item) {
mContextMenuMonitoringAction-&
gt;setData(index);
mContextMenuMonitoringAction-&
gt;setEnabled(item-&
gt;supportsMonitoring());
mContextMenuMonitoringAction-&
gt;setText(item-&
gt;monitoringEnabled() ? tr("Disable Monitoring"
) : tr("Enable Monitoring"
));
mContextMenuHistorizingAction-&
gt;setData(index);
QModelIndex isHistoricIndex =
mOpcUaModel-&
gt;index(index.row(), 7
, index.parent());
mContextMenuHistorizingAction-&
gt;setEnabled(mOpcUaModel-&
gt;data(isHistoricIndex, Qt::
DisplayRole).toString() ==
"true"
);
mContextMenu-&
gt;exec(ui-&
gt;treeView-&
gt;viewport()-&
gt;mapToGlobal(point));
}
}
}
void
MainWindow::
toggleMonitoring()
{
QModelIndex index =
mContextMenuMonitoringAction-&
gt;data().toModelIndex();
if
(index.isValid()) {
TreeItem*
item =
static_cast
&
lt;TreeItem *&
gt;(index.internalPointer());
if
(item) {
item-&
gt;setMonitoringEnabled(!
item-&
gt;monitoringEnabled());
}
}
}
void
MainWindow::
showHistorizing()
{
QModelIndex modelIndex =
mContextMenuHistorizingAction-&
gt;data().toModelIndex();
QModelIndex nodeIdIndex =
mOpcUaModel-&
gt;index(modelIndex.row(), 4
, modelIndex.parent());
QString nodeId =
mOpcUaModel-&
gt;data(nodeIdIndex, Qt::
DisplayRole).toString();
auto
request =
QOpcUaHistoryReadRawRequest(
{
QOpcUaReadItem(nodeId)}
,
QDateTime::
currentDateTime(),
QDateTime::
currentDateTime().addDays(-
2
),
5
,
false
);
mHistoryReadResponse.reset(mOpcUaClient-&
gt;readHistoryData(request));
if
(mHistoryReadResponse) {
QObject::
connect(mHistoryReadResponse.get(), &
amp;QOpcUaHistoryReadResponse::
readHistoryDataFinished,
this
, &
amp;MainWindow::
handleReadHistoryDataFinished);
QObject::
connect(mHistoryReadResponse.get(), &
amp;QOpcUaHistoryReadResponse::
stateChanged, this
, [](QOpcUaHistoryReadResponse::
State state) {
qDebug() &
lt;&
lt; "History read state changed to"
&
lt;&
lt; state;
}
);
}
else
{
qWarning() &
lt;&
lt; "Failed to request history data"
;
}
}
void
MainWindow::
handleReadHistoryDataFinished(QList&
lt;QOpcUaHistoryData&
gt; results, QOpcUa::
UaStatusCode serviceResult)
{
if
(serviceResult !=
QOpcUa::UaStatusCode::
Good) {
qWarning() &
lt;&
lt; "readHistoryData request finished with bad status code: "
&
lt;&
lt; serviceResult;
return
;
}
for
(int
i =
0
; i &
lt; results.count(); ++
i) {
qInfo() &
lt;&
lt; "NodeId:"
&
lt;&
lt; results.at(i).nodeId() &
lt;&
lt; "; statusCode:"
&
lt;&
lt; results.at(i).statusCode() &
lt;&
lt; "; returned values:"
&
lt;&
lt; results.at(i).count();
for
(int
j =
0
; j &
lt; results.at(i).count(); ++
j) {
qInfo() &
lt;&
lt; j
&
lt;&
lt; "source timestamp:"
&
lt;&
lt; results.at(i).result()[j].sourceTimestamp()
&
lt;&
lt; "server timestamp:"
&
lt;&
lt; results.at(i).result()[j].serverTimestamp()
&
lt;&
lt; "value:"
&
lt;&
lt; results.at(i).result()[j].value();
}
}
}