Qt OPC UA Viewer

 // Copyright (C) 2018 The Qt Company Ltd.
 // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

 #include "treeitem.h"
 #include "opcuamodel.h"

 #include <QOpcUaArgument>
 #include <QOpcUaAxisInformation>
 #include <QOpcUaClient>
 #include <QOpcUaComplexNumber>
 #include <QOpcUaDoubleComplexNumber>
 #include <QOpcUaEUInformation>
 #include <QOpcUaExtensionObject>
 #include <QOpcUaGenericStructHandler>
 #include <QOpcUaGenericStructValue>
 #include <QOpcUaLocalizedText>
 #include <QOpcUaNode>
 #include <QOpcUaQualifiedName>
 #include <QOpcUaRange>
 #include <QOpcUaXValue>

 #include <QMetaEnum>
 #include <QPixmap>

 using namespace Qt::Literals::StringLiterals;

 const int numberOfDisplayColumns = 8; // NodeId, Value, NodeClass, DataType, BrowseName, DisplayName, Description, Historizing

 TreeItem::TreeItem(OpcUaModel *model)
     : QObject(nullptr)
     , mModel(model)
 {
 }

 TreeItem::TreeItem(QOpcUaNode *node, OpcUaModel *model, TreeItem *parent)
     : QObject(parent)
     , mOpcNode(node)
     , mModel(model)
     , mParentItem(parent)
 {
     connect(mOpcNode.get(), &QOpcUaNode::attributeRead, this, &TreeItem::handleAttributes);
     connect(mOpcNode.get(), &QOpcUaNode::attributeUpdated, this, &TreeItem::handleAttributes);
     connect(mOpcNode.get(), &QOpcUaNode::browseFinished, this, &TreeItem::browseFinished);

     if (!mOpcNode->readAttributes( QOpcUa::NodeAttribute::Value
                             | QOpcUa::NodeAttribute::NodeClass
                             | QOpcUa::NodeAttribute::Description
                             | QOpcUa::NodeAttribute::DataType
                             | QOpcUa::NodeAttribute::BrowseName
                             | QOpcUa::NodeAttribute::DisplayName
                             | QOpcUa::NodeAttribute::Historizing
                             )) {
         qWarning() << "Reading attributes" << mOpcNode->nodeId() << "failed";
     }
 }

 TreeItem::TreeItem(QOpcUaNode *node, OpcUaModel *model, const QOpcUaReferenceDescription &browsingData, TreeItem *parent) : TreeItem(node, model, parent)
 {
     mNodeBrowseName = browsingData.browseName().name();
     mNodeClass = browsingData.nodeClass();
     mNodeId = browsingData.targetNodeId().nodeId();
     mNodeDisplayName = browsingData.displayName().text();
     mHistorizing = false;
 }

 TreeItem::~TreeItem()
 {
     qDeleteAll(mChildItems);
 }

 TreeItem *TreeItem::child(int row)
 {
     if (row >= mChildItems.size())
         qCritical() << "TreeItem in row" << row << "does not exist.";
     return mChildItems[row];
 }

 int TreeItem::childIndex(const TreeItem *child) const
 {
     return mChildItems.indexOf(const_cast<TreeItem *>(child));
 }

 int TreeItem::childCount()
 {
     startBrowsing();
     return mChildItems.size();
 }

 int TreeItem::columnCount() const
 {
     return numberOfDisplayColumns;
 }

 QVariant TreeItem::data(int column)
 {
     if (column == 0)
         return mNodeBrowseName;
     if (column == 1) {
         if (!mAttributesReady)
             return tr("Loading ...");

         const auto type = mOpcNode->attribute(QOpcUa::NodeAttribute::DataType).toString();
         const auto value = mOpcNode->attribute(QOpcUa::NodeAttribute::Value);

         return variantToString(value, type);
     }
     if (column == 2) {
         QMetaEnum metaEnum = QMetaEnum::fromType<QOpcUa::NodeClass>();
         QString name = metaEnum.valueToKey(int(mNodeClass));
         return name + " (" + QString::number(int(mNodeClass)) + ')';
     }
     if (column == 3) {
         if (!mAttributesReady)
             return tr("Loading ...");

         const QString typeId = mOpcNode->attribute(QOpcUa::NodeAttribute::DataType).toString();
         auto enumEntry = QOpcUa::namespace0IdFromNodeId(typeId);
         if (enumEntry == QOpcUa::NodeIds::Namespace0::Unknown)
             return typeId;
         return QOpcUa::namespace0IdName(enumEntry) + " (" + typeId + ")";
     }
     if (column == 4)
         return mNodeId;
     if (column == 5)
         return mNodeDisplayName;
     if (column == 6) {
         return mAttributesReady
             ? mOpcNode->attribute(QOpcUa::NodeAttribute::Description).value<QOpcUaLocalizedText>().text()
             : tr("Loading ...");
     }
     if (column == 7) {
         return mAttributesReady
             ? mHistorizing ? QString("true") : QString("false") : tr("Loading ...");
     }
     return QVariant();
 }

 int TreeItem::row() const
 {
     if (!mParentItem)
         return 0;
     return mParentItem->childIndex(this);
 }

 TreeItem *TreeItem::parentItem()
 {
     return mParentItem;
 }

 void TreeItem::appendChild(TreeItem *child)
 {
     if (!child)
         return;

     if (!hasChildNodeItem(child->mNodeId)) {
         mChildItems.append(child);
         mChildNodeIds.insert(child->mNodeId);
     } else {
         child->deleteLater();
     }
 }

 static QPixmap createPixmap(const QColor &c)
 {
     QPixmap p(10,10);
     p.fill(c);
     return p;
 }

 QPixmap TreeItem::icon(int column) const
 {
     if (column != 0 || !mOpcNode)
         return QPixmap();

     static const QPixmap objectPixmap = createPixmap(Qt::darkGreen);
     static const QPixmap variablePixmap = createPixmap(Qt::darkBlue);
     static const QPixmap methodPixmap = createPixmap(Qt::darkRed);
     static const QPixmap defaultPixmap = createPixmap(Qt::gray);

     switch (mNodeClass) {
     case QOpcUa::NodeClass::Object:
         return objectPixmap;
     case QOpcUa::NodeClass::Variable:
         return variablePixmap;
     case QOpcUa::NodeClass::Method:
         return methodPixmap;
     default:
         break;
     }

     return defaultPixmap;
 }

 bool TreeItem::hasChildNodeItem(const QString &nodeId) const
 {
     return mChildNodeIds.contains(nodeId);
 }

 void TreeItem::setMonitoringEnabled(bool active)
 {
     if (!supportsMonitoring())
         return;

     if (active) {
         mOpcNode->enableMonitoring(QOpcUa::NodeAttribute::Value, QOpcUaMonitoringParameters(500));
     } else {
         mOpcNode->disableMonitoring(QOpcUa::NodeAttribute::Value);
     }
 }

 bool TreeItem::monitoringEnabled() const
 {
     QOpcUaMonitoringParameters monitoring = mOpcNode.get()->monitoringStatus(QOpcUa::NodeAttribute::Value);
     return  monitoring.statusCode() == QOpcUa::UaStatusCode::Good &&
             monitoring.monitoringMode() == QOpcUaMonitoringParameters::MonitoringMode::Reporting;
 }

 bool TreeItem::supportsMonitoring() const
 {
     return mNodeClass == QOpcUa::NodeClass::Variable;
 }

 void TreeItem::startBrowsing()
 {
     if (mBrowseStarted)
         return;

     if (!mOpcNode->browseChildren())
         qWarning() << "Browsing node" << mOpcNode->nodeId() << "failed";
     else
         mBrowseStarted = true;
 }

 void TreeItem::handleAttributes(QOpcUa::NodeAttributes attr)
 {
     if (attr & QOpcUa::NodeAttribute::NodeClass)
         mNodeClass = mOpcNode->attribute(QOpcUa::NodeAttribute::NodeClass).value<QOpcUa::NodeClass>();
     if (attr & QOpcUa::NodeAttribute::BrowseName)
         mNodeBrowseName = mOpcNode->attribute(QOpcUa::NodeAttribute::BrowseName).value<QOpcUaQualifiedName>().name();
     if (attr & QOpcUa::NodeAttribute::DisplayName)
         mNodeDisplayName = mOpcNode->attribute(QOpcUa::NodeAttribute::DisplayName).value<QOpcUaLocalizedText>().text();
     if (attr & QOpcUa::NodeAttribute::Historizing) {
         mHistorizing = mOpcNode->attribute(QOpcUa::NodeAttribute::Historizing).value<bool>();
     }

     mAttributesReady = true;
     emit mModel->dataChanged(mModel->createIndex(row(), 0, this), mModel->createIndex(row(), numberOfDisplayColumns - 1, this));
 }

 void TreeItem::browseFinished(const QList<QOpcUaReferenceDescription> &children, QOpcUa::UaStatusCode statusCode)
 {
     if (statusCode != QOpcUa::Good) {
         qWarning() << "Browsing node" << mOpcNode->nodeId() << "finally failed:" << statusCode;
         return;
     }

     auto index = mModel->createIndex(row(), 0, this);

     for (const auto &item : children) {
         if (hasChildNodeItem(item.targetNodeId().nodeId()))
             continue;

         auto node = mModel->opcUaClient()->node(item.targetNodeId());
         if (!node) {
             qWarning() << "Failed to instantiate node:" << item.targetNodeId().nodeId();
             continue;
         }

         mModel->beginInsertRows(index, mChildItems.size(), mChildItems.size() + 1);
         appendChild(new TreeItem(node, mModel, item, this));
         mModel->endInsertRows();
     }

     emit mModel->dataChanged(mModel->createIndex(row(), 0, this), mModel->createIndex(row(), numberOfDisplayColumns - 1, this));
 }

 QString TreeItem::variantToString(const QVariant &value, const QString &typeNodeId) const
 {
     if (value.metaType().id() == QMetaType::QVariantList) {
         const auto list = value.toList();
         QString concat;
         for (int i = 0, size = list.size(); i < size; ++i) {
             if (i)
                 concat.append('\n'_L1);
             concat.append(variantToString(list.at(i), typeNodeId));
         }
         return concat;
     }

     if (typeNodeId == "ns=0;i=19"_L1) { // StatusCode
         const char *name = QMetaEnum::fromType<QOpcUa::UaStatusCode>().valueToKey(value.toInt());
         return name ? QLatin1StringView(name) : "Unknown StatusCode"_L1;
     }
     if (typeNodeId == "ns=0;i=2"_L1) // Char
         return QString::number(value.toInt());
     if (typeNodeId == "ns=0;i=3"_L1) // SChar
         return QString::number(value.toUInt());
     if (typeNodeId == "ns=0;i=4"_L1) // Int16
         return QString::number(value.toInt());
     if (typeNodeId == "ns=0;i=5"_L1) // UInt16
         return QString::number(value.toUInt());
     if (value.metaType().id() == QMetaType::QByteArray)
         return "0x"_L1 + value.toByteArray().toHex();
     if (value.metaType().id() == QMetaType::QDateTime)
         return value.toDateTime().toString(Qt::ISODate);
     if (value.canConvert<QOpcUaQualifiedName>()) {
         const auto name = value.value<QOpcUaQualifiedName>();
         return u"[NamespaceIndex: %1, Name: \"%2\"]"_s.arg(name.namespaceIndex()).arg(name.name());
     }
     if (value.canConvert<QOpcUaLocalizedText>()) {
         const auto text = value.value<QOpcUaLocalizedText>();
         return localizedTextToString(text);
     }
     if (value.canConvert<QOpcUaRange>()) {
         const auto range = value.value<QOpcUaRange>();
         return rangeToString(range);
     }
     if (value.canConvert<QOpcUaComplexNumber>()) {
         const auto complex = value.value<QOpcUaComplexNumber>();
         return u"[Real: %1, Imaginary: %2]"_s.arg(complex.real()).arg(complex.imaginary());
     }
     if (value.canConvert<QOpcUaDoubleComplexNumber>()) {
         const auto complex = value.value<QOpcUaDoubleComplexNumber>();
         return u"[Real: %1, Imaginary: %2]"_s.arg(complex.real()).arg(complex.imaginary());
     }
     if (value.canConvert<QOpcUaXValue>()) {
         const auto xv = value.value<QOpcUaXValue>();
         return u"[X: %1, Value: %2]"_s.arg(xv.x()).arg(xv.value());
     }
     if (value.canConvert<QOpcUaEUInformation>()) {
         const auto info = value.value<QOpcUaEUInformation>();
         return euInformationToString(info);
     }
     if (value.canConvert<QOpcUaAxisInformation>()) {
         const auto info = value.value<QOpcUaAxisInformation>();
         return u"[EUInformation: %1, EURange: %2, Title: %3 , AxisScaleType: %4, AxisSteps: %5]"_s.arg(
             euInformationToString(info.engineeringUnits()), rangeToString(info.eURange()), localizedTextToString(info.title()),
             info.axisScaleType() == QOpcUa::AxisScale::Linear ? "Linear" : (info.axisScaleType() == QOpcUa::AxisScale::Ln) ? "Ln" : "Log",
             numberArrayToString(info.axisSteps()));
     }
     if (value.canConvert<QOpcUaExpandedNodeId>()) {
         const auto id = value.value<QOpcUaExpandedNodeId>();
         return u"[NodeId: \"%1\", ServerIndex: \"%2\", NamespaceUri: \"%3\"]"_s.arg(
                     id.nodeId()).arg(id.serverIndex()).arg(id.namespaceUri());
     }
     if (value.canConvert<QOpcUaArgument>()) {
         const auto a = value.value<QOpcUaArgument>();

         return u"[Name: \"%1\", DataType: \"%2\", ValueRank: \"%3\", ArrayDimensions: %4, Description: %5]"_s.arg(
                     a.name(), a.dataTypeId()).arg(a.valueRank()).arg(numberArrayToString(a.arrayDimensions()),
                     localizedTextToString(a.description()));
     }
     if (value.canConvert<QOpcUaExtensionObject>()) {
         auto obj = value.value<QOpcUaExtensionObject>();

         if (mModel->genericStructHandler() &&
             mModel->genericStructHandler()->dataTypeKindForTypeId(mModel->genericStructHandler()->typeIdForBinaryEncodingId(obj.encodingTypeId()))
                                                   == QOpcUaGenericStructHandler::DataTypeKind::Struct) {
             const auto decodedValue = mModel->genericStructHandler()->decode(obj);
             if (decodedValue)
                 return decodedValue->toString();
         }

         return u"[TypeId: \"%1\", Encoding: %2, Body: 0x%3]"_s.arg(obj.encodingTypeId(),
                     obj.encoding() == QOpcUaExtensionObject::Encoding::NoBody ?
                         "NoBody" : (obj.encoding() == QOpcUaExtensionObject::Encoding::ByteString ?
                             "ByteString" : "XML")).arg(obj.encodedBody().isEmpty() ? "0" : QString(obj.encodedBody().toHex()));
     }

     if (value.canConvert<QString>())
         return value.toString();

     return QString();
 }

 QString TreeItem::localizedTextToString(const QOpcUaLocalizedText &text) const
 {
     return u"[Locale: \"%1\", Text: \"%2\"]"_s.arg(text.locale(), text.text());
 }

 QString TreeItem::rangeToString(const QOpcUaRange &range) const
 {
     return u"[Low: %1, High: %2]"_s.arg(range.low()).arg(range.high());
 }

 QString TreeItem::euInformationToString(const QOpcUaEUInformation &info) const
 {
     return u"[UnitId: %1, NamespaceUri: \"%2\", DisplayName: %3, Description: %4]"_s.arg(info.unitId()).arg(
                 info.namespaceUri(), localizedTextToString(info.displayName()), localizedTextToString(info.description()));
 }