{"id":9419,"date":"2024-07-11T20:07:35","date_gmt":"2024-07-11T18:07:35","guid":{"rendered":"https:\/\/www.basyskom.de\/?p=9419"},"modified":"2024-10-08T11:16:48","modified_gmt":"2024-10-08T09:16:48","slug":"opc-ua-programming-against-type-descriptions","status":"publish","type":"post","link":"https:\/\/www.basyskom.de\/en\/opc-ua-programming-against-type-descriptions\/","title":{"rendered":"OPC UA: Programming against Type Descriptions"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-post\" data-elementor-id=\"9419\" class=\"elementor elementor-9419\" data-elementor-post-type=\"post\">\n\t\t\t\t<div class=\"elementor-element elementor-element-327281d e-con-full e-flex wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-equal-height-no e-con e-parent\" data-id=\"327281d\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t<div class=\"elementor-element elementor-element-62aa28d elementor-widget elementor-widget-heading\" data-id=\"62aa28d\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"heading.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<h2 class=\"elementor-heading-title elementor-size-default\">Addressing OPC UA Nodes<\/h2>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-179f0be elementor-widget elementor-widget-text-editor\" data-id=\"179f0be\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<p>Information in OPC UA servers is stored in so-called &#8220;nodes&#8221;. Every node is identified by a <a href=\"https:\/\/reference.opcfoundation.org\/Core\/Part3\/v105\/docs\/8.2\" target=\"_blank\" rel=\"noopener\">NodeId<\/a>\u00a0which is used to address the node in OPC UA service calls (e.g. read\/write\/method call). So a client that wants to access the nodes of a server needs to find out about the NodeIds involved. While it might be tempting to launch a graphical browser like <em>UaExpert<\/em> to find the nodes you need and to copy their NodeIds into your application code, this is most often a bad idea.<\/p><p>This article explains why this is the case and shows the preferable approach, leading to more robust, portable client code.<\/p>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-db81a79 elementor-widget elementor-widget-heading\" data-id=\"db81a79\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"heading.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<h2 class=\"elementor-heading-title elementor-size-default\">Introduction to the OPC UA Address Space<\/h2>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-545dc63 elementor-widget elementor-widget-text-editor\" data-id=\"545dc63\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<p>The address space of an OPC UA server is a full mesh network of nodes connected by references.<\/p><p>Nodes are grouped into different <i>NodeClasses<\/i> (e.g. <i>Object<\/i>, <i>ObjectType<\/i>, <i>Variable<\/i>, &#8230;) and have\u00a0<span style=\"text-align: var(--text-align); color: var( --e-global-color-f7d9691 );\">a set of data elements named <em>attributes<\/em> that can be read and written. Some attributes like <i>NodeId<\/i>, <i>BrowseName<\/i> and <i>DisplayName<\/i> are present for all<\/span><span style=\"text-align: var(--text-align); color: var( --e-global-color-f7d9691 );\">\u00a0<\/span><i style=\"text-align: var(--text-align); color: var( --e-global-color-f7d9691 );\">NodeClasses<\/i> while others like <i>Value<\/i> or <i>EventNotifier<\/i> are <i>NodeClass<\/i> specific.<\/p><p>A server&#8217;s address space is divided into different <i>namespaces<\/i> that are identified by a URI and are listed in the <i>Namespaces<\/i> array node of the <i>Server<\/i> object. The array index of a namespace is used as part of the <a href=\"https:\/\/reference.opcfoundation.org\/Core\/Part3\/v105\/docs\/8.2\" target=\"_blank\" rel=\"noopener\"><i>NodeId<\/i><\/a> type in the <i>NodeId<\/i> attribute of every node to assign it to a <i>namespace<\/i>. The identifier part of the <i>NodeId<\/i> can be an <i>integer<\/i>, a <i>string<\/i>, a <i>GUID<\/i>\u00a0or an opaque <i>ByteString<\/i> and must be unique in the context of its namespace.<\/p><p>References connect nodes to other nodes and have a semantic meaning expressed by their name. For example, an <i>Object<\/i> node of the type <i>FolderType<\/i> has <i>Organizes<\/i> references to the nodes it contains. An <i>Object<\/i> node modelling a complex real world object usually references its children using\u00a0<i>HasComponent<\/i>. References may also connect nodes in different namespaces.<\/p>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-9ddcdcf elementor-widget elementor-widget-heading\" data-id=\"9ddcdcf\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"heading.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<h2 class=\"elementor-heading-title elementor-size-default\">Why not to use hardcoded NodeIds<\/h2>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-8663ebb elementor-widget elementor-widget-text-editor\" data-id=\"8663ebb\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<p>While there are some well-known NodeIds defined by the OPC UA standard or a companion specification like the <i>Objects<\/i> folder, the <i>Server<\/i> object or the <i>Machines<\/i> folder from the Machinery CS, most of the server specific nodes containing data are not guaranteed to always have the same NodeId<span style=\"text-align: var(--text-align); color: var( --e-global-color-f7d9691 );\">. Some servers will use string NodeIds representing a path (e.g.\u00a0<\/span><i style=\"text-align: var(--text-align); color: var( --e-global-color-f7d9691 );\">MyMachine.Motor.RPM<\/i><span style=\"text-align: var(--text-align); color: var( --e-global-color-f7d9691 );\">) or implement some mechanism to generate numeric NodeIds to keep them somewhat static, but most servers will use random integer NodeIds that might be completely different after a restart. A server might also expose different objects depending on currently connected hardware or the availability of other external data sources.<\/span><\/p><p>Also if your application needs to work with any server implementing a certain companion specification, there is no guarantee that you\u2019ll always find the companion specification\u2019s namespace on the same index in the namespaces array.<\/p><p>To summarize: embedding hardcoded NodeIds in your client code will make your application less robust, less portable and harder to maintain.<\/p>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-66bb492 elementor-widget elementor-widget-heading\" data-id=\"66bb492\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"heading.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<h2 class=\"elementor-heading-title elementor-size-default\">What to do?<\/h2>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-38d7b43 elementor-widget elementor-widget-text-editor\" data-id=\"38d7b43\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<p><span style=\"text-align: var(--text-align); color: var( --e-global-color-f7d9691 );\">OPC UA nodes of all classes have two naming related attributes. While the <em>DisplayName<\/em> contains a <a href=\"https:\/\/reference.opcfoundation.org\/Core\/Part3\/v105\/docs\/8.5\" target=\"_blank\" rel=\"noopener\"><em>LocalizedText<\/em><\/a> name for display purposes, which usually describes the node&#8217;s function in one word or a short <i>CamelCase<\/i> string, the\u00a0<\/span><a href=\"https:\/\/reference.opcfoundation.org\/Core\/Part3\/v105\/docs\/5.2.4\" target=\"_blank\" rel=\"noopener\"><i style=\"text-align: var(--text-align); color: var( --e-global-color-f7d9691 );\">BrowseName<\/i><\/a><span style=\"text-align: var(--text-align); color: var( --e-global-color-f7d9691 );\"> attribute holds a <\/span><em><a style=\"text-align: var(--text-align); font-family: pt-sans-v17-latin-regular, pt-sans-narrow; font-size: 18px; font-weight: normal; background-color: #ffffff;\" href=\"https:\/\/reference.opcfoundation.org\/Core\/Part3\/v105\/docs\/8.3\" target=\"_blank\" rel=\"noopener\">QualifiedName<\/a>,<\/em><span style=\"text-align: var(--text-align); color: var( --e-global-color-f7d9691 );\">\u00a0which consists of a namespace index and a name string. It should be unique on its level in the server&#8217;s node hierarchy. Its intended usage is building<\/span><span style=\"color: var( --e-global-color-f7d9691 ); text-align: var(--text-align);\">\u00a0<\/span><a style=\"background-color: #ffffff; font-family: pt-sans-v17-latin-regular, pt-sans-narrow; font-size: 18px; font-weight: normal; line-height: var( --e-global-typography-text-line-height ); text-align: var(--text-align);\" href=\"https:\/\/reference.opcfoundation.org\/Core\/Part4\/v105\/docs\/7.31\" target=\"_blank\" rel=\"noopener\">RelativePaths<\/a>,<span style=\"text-align: var(--text-align); color: var( --e-global-color-f7d9691 );\">\u00a0which consist of <em>BrowseNames<\/em> and information about the reference connecting the node to the previous node.<\/span><\/p><p>As each model in the server has its own namespace, the <i>BrowseName&#8217;<\/i>s namespace index indicates which model a node is from. If a model defines a new object type, variable type or event type with child nodes, all of them have a <i>BrowseName<\/i> from its namespace. If a model extends an existing type from a different model by inheritance, only the newly added child nodes will have its namespace index in the <i>BrowseName<\/i> while the inherited children keep their namespace index.<\/p><p><span style=\"text-align: var(--text-align); color: var( --e-global-color-f7d9691 );\">The <i>RelativePath<\/i> concept lends itself to programming against type definitions instead of specific nodes identified by their<\/span><span style=\"text-align: var(--text-align); color: var( --e-global-color-f7d9691 );\">\u00a0<\/span><i style=\"text-align: var(--text-align); color: var( --e-global-color-f7d9691 );\">NodeId<\/i><span style=\"text-align: var(--text-align); color: var( --e-global-color-f7d9691 );\">\u00a0<\/span><span style=\"text-align: var(--text-align); color: var( --e-global-color-f7d9691 );\">because there is always an unambiguous path from the type&#8217;s top level node to each child node deeper in the hierarchy.<\/span><\/p>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-8e4642a elementor-widget elementor-widget-image\" data-id=\"8e4642a\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"image.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<img fetchpriority=\"high\" decoding=\"async\" width=\"531\" height=\"405\" src=\"https:\/\/www.basyskom.de\/wp-content\/uploads\/2024\/07\/attributes.png\" class=\"attachment-large size-large wp-image-9901\" alt=\"basysKom, HMI Dienstleistung, Qt, Cloud, Azure\" srcset=\"https:\/\/www.basyskom.de\/wp-content\/uploads\/2024\/07\/attributes.png 531w, https:\/\/www.basyskom.de\/wp-content\/uploads\/2024\/07\/attributes-300x229.png 300w, https:\/\/www.basyskom.de\/wp-content\/uploads\/2024\/07\/attributes-16x12.png 16w\" sizes=\"(max-width: 531px) 100vw, 531px\">\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-1aeefcf elementor-widget elementor-widget-text-editor\" data-id=\"1aeefcf\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<p><span style=\"text-align: var(--text-align); color: var( --e-global-color-f7d9691 );\">To put this knowledge into practice, OPC UA provides the\u00a0<\/span><a style=\"background-color: #ffffff; font-family: pt-sans-v17-latin-regular, pt-sans-narrow; font-size: 18px; font-weight: normal; line-height: var( --e-global-typography-text-line-height ); text-align: var(--text-align);\" href=\"https:\/\/reference.opcfoundation.org\/Core\/Part4\/v105\/docs\/5.8.4\" target=\"_blank\" rel=\"noopener\">TranslateBrowsePathsToNodeIds<\/a><span style=\"text-align: var(--text-align); color: var( --e-global-color-f7d9691 );\">\u00a0service, which takes a start NodeId and a\u00a0<\/span><i>RelativePath<\/i><span style=\"text-align: var(--text-align); color: var( --e-global-color-f7d9691 );\">. The last <em>RelativePathElement&#8217;s<\/em>\u00a0<em>BrowseName<\/em> may be left empty, which will return all child nodes matching the given reference information. The NodeId(s) obtained from this service can then be used in any OPC UA service.<\/span><\/p><p><span style=\"text-align: var(--text-align); color: var( --e-global-color-f7d9691 );\">Most companion specifications define a point of entry like the <em>Machines<\/em> folder that references the server specific objects that implement a certain object type from that specification. In order to find interesting objects, the <a href=\"https:\/\/reference.opcfoundation.org\/Core\/Part4\/v105\/docs\/5.8.2\" target=\"_blank\" rel=\"noopener\">Browse<\/a> service can be called to return all NodeIds and BrowseNames of <em>Object<\/em>\u00a0nodes referenced by the point of entry along with the NodeId of their <em>TypeDefinition<\/em> node. This information is then used as a starting point for <em>TranslateBrowsePathsToNodeIds<\/em> calls to find the object&#8217;s child nodes according to its type.<\/span><\/p><p>The <em>Browse<\/em> and <i>TranslateBrowsePathsToNodeIds<\/i>\u00a0services used in conjunction with the server&#8217;s namespace array to resolve the URIs of the involved models to a namespace index are a mighty tool to find a certain node on any server implementing a given specification.<\/p>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-4f61ed6 elementor-widget elementor-widget-heading\" data-id=\"4f61ed6\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"heading.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<h2 class=\"elementor-heading-title elementor-size-default\">Example with Qt OPC UA<\/h2>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-6c99b8a elementor-widget elementor-widget-text-editor\" data-id=\"6c99b8a\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<p>The following example demonstrates how to find some selected <i>Identification<\/i> properties of all woodworking machines on the <a href=\"https:\/\/github.com\/umati\/Sample-Server?tab=readme-ov-file#public-available-instance\" target=\"_blank\" rel=\"noopener\">public umati sample server<\/a> at <i>opc.tcp:\/\/opcua.umati.app<\/i> using Qt OPC UA<i>.<\/i> This server contains simulated instances of machines for most companion specifications where the\u00a0<a href=\"https:\/\/umati.org\/\" target=\"_blank\" rel=\"noopener\">umati initiative<\/a>\u00a0is involved and has proven a handy tool to test OPC UA enabled client applications implementing these specifications.<\/p><p>The server provides two simulated woodworking machines:<\/p><ul style=\"font-size: 18px; background-color: #ffffff;\"><li style=\"font-size: 18px;\"><em>BasicWoodworking<\/em>\u00a0only contains the mandatory child nodes of\u00a0<i>WwMachineType<\/i><\/li><li style=\"font-size: 18px;\"><i>FullWoodworking<\/i>\u00a0contains all child nodes (mandatory and optional)<\/li><\/ul>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-7419cd5 elementor-widget elementor-widget-image\" data-id=\"7419cd5\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"image.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<img decoding=\"async\" width=\"220\" height=\"355\" src=\"https:\/\/www.basyskom.de\/wp-content\/uploads\/2024\/07\/wwmachines.png\" class=\"attachment-large size-large wp-image-9825\" alt=\"basysKom, HMI Dienstleistung, Qt, Cloud, Azure\" srcset=\"https:\/\/www.basyskom.de\/wp-content\/uploads\/2024\/07\/wwmachines.png 220w, https:\/\/www.basyskom.de\/wp-content\/uploads\/2024\/07\/wwmachines-186x300.png 186w, https:\/\/www.basyskom.de\/wp-content\/uploads\/2024\/07\/wwmachines-7x12.png 7w\" sizes=\"(max-width: 220px) 100vw, 220px\">\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-e166846 elementor-widget elementor-widget-text-editor\" data-id=\"e166846\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<p><span style=\"text-align: var(--text-align); color: var( --e-global-color-f7d9691 );\">Resolving NodeIds on the server requires several steps:<\/span><\/p><ul><li>Read the namespaces array (this is done automatically by <i>QOpcUaClient<\/i> on connect)<\/li><li>Build the NodeId of the <i>Machines<\/i> folder from the <i>Machinery<\/i> namespace URI&#8217;s index in the namespaces array and the well known numeric identifier, browse its object children and identify all objects of type <i style=\"background-color: transparent; text-align: var(--text-align); color: var( --e-global-color-f7d9691 );\">WwMachineType<\/i><\/li><li>Build the <i>BrowsePath<\/i> elements from the corresponding namespaces URIs&#8217;s indices in the namespaces array and invoke the <i>TranslateBrowsePathsToNodeIds<\/i> service (<i>resolveBrowsePath()<\/i>\u00a0of <i>QOpcUaNode<\/i>)<\/li><li>Print the resulting NodeIds, if found (<i>LocationGPS<\/i> is optional and only present in <i>FullWoodworking<\/i>)<\/li><li>Do something with the found NodeIds (in this example, read the value attribute)<\/li><\/ul>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-3b427a6 elementor-widget elementor-widget-elementor-syntax-highlighter\" data-id=\"3b427a6\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"elementor-syntax-highlighter.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<pre><code class='language-cpp'>#include &lt;QCoreApplication&gt;\n\n#include &lt;QOpcUaClient&gt;\n#include &lt;QOpcUaProvider&gt;\n\nclass OpcUaDemo : public QObject\n{\n    Q_OBJECT\n\npublic:\n    bool init();\n\nsignals:\n    void done();\n\nprivate:\n    void handleEndpoints(const QList&lt;QOpcUaEndpointDescription&gt; &amp;endpoints,\n                         QOpcUa::UaStatusCode statusCode, const QUrl &amp;requestUrl);\n    void handleStateChanged(QOpcUaClient::ClientState state);\n    void handleNamespaceArrayUpdated(const QStringList &amp;namespaces);\n\n    void findWoodworkingMachines();\n    void findWoodworkingIdentification(const QString &amp;nodeId, const QString &amp;machineName);\n\n    void readAndPrintValueAttribute(const QString &amp;nodeId, const QString &amp;machineName,\n                                    const QString &amp;propertyName);\n\n    QScopedPointer&lt;QOpcUaClient&gt; m_client;\n    std::vector&lt;std::unique_ptr&lt;QOpcUaNode&gt;&gt; m_nodes;\n    int m_pendingNodes = 0;\n\n    const QString DiUri = QStringLiteral(&quot;http:\/\/opcfoundation.org\/UA\/DI\/&quot;);\n    const QString MachineryUri = QStringLiteral(&quot;http:\/\/opcfoundation.org\/UA\/Machinery\/&quot;);\n    const QString WoodworkingUri = QStringLiteral(&quot;http:\/\/opcfoundation.org\/UA\/Woodworking\/&quot;);\n};\n\nbool OpcUaDemo::init()\n{\n    QOpcUaProvider provider;\n    if (provider.availableBackends().empty()) {\n        qWarning(&quot;No backends available&quot;);\n        return false;\n    }\n\n    m_client.reset(provider.createClient(provider.availableBackends().constFirst()));\n\n    if (!m_client) {\n        qWarning(&quot;Failed to create client&quot;);\n        return false;\n    }\n\n    QObject::connect(m_client.get(), &amp;QOpcUaClient::endpointsRequestFinished,\n                     this, &amp;OpcUaDemo::handleEndpoints);\n    QObject::connect(m_client.get(), &amp;QOpcUaClient::stateChanged,\n                     this, &amp;OpcUaDemo::handleStateChanged);\n    QObject::connect(m_client.get(), &amp;QOpcUaClient::namespaceArrayUpdated,\n                     this, &amp;OpcUaDemo::handleNamespaceArrayUpdated);\n\n    return m_client-&gt;requestEndpoints(QStringLiteral(&quot;opc.tcp:\/\/opcua.umati.app&quot;));\n}\n\nvoid OpcUaDemo::handleEndpoints(const QList&lt;QOpcUaEndpointDescription&gt; &amp;endpoints,\n                                QOpcUa::UaStatusCode statusCode, const QUrl &amp;requestUrl)\n{\n    for (const auto &amp;endpoint : endpoints) {\n        if (endpoint.securityMode() == QOpcUaEndpointDescription::None)\n            return m_client-&gt;connectToEndpoint(endpoint);\n    }\n\n    qWarning(&quot;No suitable endpoint found&quot;);\n    emit done();\n}\n\nvoid OpcUaDemo::handleStateChanged(QOpcUaClient::ClientState state)\n{\n    qDebug() &lt;&lt; &quot;State changed to&quot; &lt;&lt; state;\n\n    if (state == QOpcUaClient::ClientState::Disconnected)\n        emit done();\n}\n\nvoid OpcUaDemo::handleNamespaceArrayUpdated(const QStringList &amp;namespaces)\n{\n    \/\/ Make sure all required namespaces are present on the server\n    if (!namespaces.contains(DiUri)) {\n        qWarning() &lt;&lt; &quot;Could not find namespace&quot; &lt;&lt; DiUri;\n        emit done();\n    }\n\n    if (!namespaces.contains(MachineryUri)) {\n        qWarning() &lt;&lt; &quot;Could not find namespace&quot; &lt;&lt; MachineryUri;\n        emit done();\n    }\n\n    if (!namespaces.contains(WoodworkingUri)) {\n        qWarning() &lt;&lt; &quot;Could not find namespace&quot; &lt;&lt; WoodworkingUri;\n        emit done();\n    }\n\n    findWoodworkingMachines();\n}\n\nvoid OpcUaDemo::findWoodworkingMachines()\n{\n    \/\/ The well-known NodeId of the Machines folder defined by the Machinery nodeset\n    const auto node = m_client-&gt;node(QOpcUa::nodeIdFromInteger(m_client-&gt;namespaceArray().indexOf(MachineryUri),\n                                                               1001));\n\n    if (!node) {\n        qWarning() &lt;&lt; &quot;Failed to create node for the Machines folder&quot;;\n        emit done();\n        return;\n    }\n\n    m_nodes.push_back(std::unique_ptr&lt;QOpcUaNode&gt;(node));\n\n    QObject::connect(node, &amp;QOpcUaNode::browseFinished,\n                     this, [this](const QList&lt;QOpcUaReferenceDescription&gt; &amp;children,\n                            QOpcUa::UaStatusCode statusCode) {\n                         bool match = false;\n                         \/\/ Well known NodeId of the WwMachineType object type\n                         const auto wwMachineTypeId =\n                             QOpcUa::nodeIdFromInteger(m_client-&gt;namespaceArray().indexOf(WoodworkingUri), 2);\n                         for (const auto &amp;child : children) {\n                             if (child.typeDefinition() == wwMachineTypeId) {\n                                 qDebug() &lt;&lt; &quot;Found woodworking machine&quot; &lt;&lt; child.browseName().name()\n                                          &lt;&lt; child.targetNodeId().nodeId();\n                                 findWoodworkingIdentification(child.targetNodeId().nodeId(),\n                                                               child.browseName().name());\n                                 match = true;\n                             }\n                         }\n\n                         if (!match) {\n                             qWarning() &lt;&lt; &quot;No Woodworking machine found&quot;;\n                             emit done();\n                             return;\n                         }\n                     });\n\n    QOpcUaBrowseRequest request;\n    request.setReferenceTypeId(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::Organizes));\n    request.setNodeClassMask(QOpcUa::NodeClass::Object);\n\n    if (!node-&gt;browse(request)) {\n        qWarning() &lt;&lt; &quot;Failed to dispatch browse request&quot;;\n        emit done();\n        return;\n    }\n}\n\nvoid OpcUaDemo::findWoodworkingIdentification(const QString &amp;nodeId, const QString &amp;machineName)\n{\n    const auto node = m_client-&gt;node(nodeId);\n\n    if (!node) {\n        qWarning() &lt;&lt; &quot;Failed to create node for&quot; &lt;&lt; nodeId;\n        emit done();\n        return;\n    }\n\n    m_nodes.push_back(std::unique_ptr&lt;QOpcUaNode&gt;(node));\n\n    QObject::connect(node, &amp;QOpcUaNode::resolveBrowsePathFinished, this,\n                     [this, machineName](const QList&lt;QOpcUaBrowsePathTarget&gt; &amp;targets,\n                                         const QList&lt;QOpcUaRelativePathElement&gt; &amp;path,\n                                         QOpcUa::UaStatusCode statusCode) {\n                         if (targets.empty()) {\n                             qWarning() &lt;&lt; &quot;Node&quot; &lt;&lt; machineName &lt;&lt; &quot;=&gt;&quot; &lt;&lt; path.back().targetName().name()\n                                        &lt;&lt; &quot;was not found&quot;;\n                         } else if (!targets.first().isFullyResolved()) {\n                             qWarning() &lt;&lt; &quot;Node&quot; &lt;&lt; machineName &lt;&lt; &quot;=&gt;&quot; &lt;&lt; path.back().targetName().name()\n                                        &lt;&lt; &quot;was not fully resolved&quot;;\n                             return;\n                         } else {\n                             qDebug() &lt;&lt; &quot;Node&quot; &lt;&lt; machineName &lt;&lt; &quot;=&gt;&quot; &lt;&lt; path.back().targetName().name()\n                                      &lt;&lt; &quot;has NodeId&quot; &lt;&lt; targets.first().targetId().nodeId();\n                             readAndPrintValueAttribute(targets.first().targetId().nodeId(),\n                                                        machineName, path.back().targetName().name());\n                         }\n\n                         if (--m_pendingNodes == 0)\n                             emit done();\n                     });\n\n    \/\/ The BrowseNames of the Identification folder&#039;s child nodes to resolve\n    const QList&lt;QOpcUaQualifiedName&gt; nodeNames {\n        m_client-&gt;qualifiedNameFromNamespaceUri(DiUri, QStringLiteral(&quot;Manufacturer&quot;)),\n        m_client-&gt;qualifiedNameFromNamespaceUri(DiUri, QStringLiteral(&quot;SerialNumber&quot;)),\n        m_client-&gt;qualifiedNameFromNamespaceUri(WoodworkingUri, QStringLiteral(&quot;LocationGPS&quot;)),\n        m_client-&gt;qualifiedNameFromNamespaceUri(MachineryUri, QStringLiteral(&quot;YearOfConstruction&quot;)),\n    };\n\n    \/\/ The BrowseName of the Identification folder of the machine\n    const auto identificationName = m_client-&gt;qualifiedNameFromNamespaceUri(DiUri, QStringLiteral(&quot;Identification&quot;));\n    for (const auto &amp;name : nodeNames) {\n        const QList&lt;QOpcUaRelativePathElement&gt; path {\n            QOpcUaRelativePathElement(identificationName, QOpcUa::ReferenceTypeId::HasAddIn),\n            QOpcUaRelativePathElement(name, QOpcUa::ReferenceTypeId::HasProperty),\n        };\n\n\n        if (!node-&gt;resolveBrowsePath(path)) {\n            qWarning() &lt;&lt; &quot;Failed to dispatch resolve request&quot;;\n            emit done();\n            return;\n        }\n\n        ++m_pendingNodes;\n    }\n}\n\nvoid OpcUaDemo::readAndPrintValueAttribute(const QString &amp;nodeId, const QString &amp;machineName,\n                                           const QString &amp;propertyName)\n{\n    const auto node = m_client-&gt;node(nodeId);\n\n    if (!node) {\n        qWarning() &lt;&lt; &quot;Failed to create node for&quot; &lt;&lt; nodeId;\n        emit done();\n        return;\n    }\n\n    m_nodes.push_back(std::unique_ptr&lt;QOpcUaNode&gt;(node));\n\n    QObject::connect(node, &amp;QOpcUaNode::attributeRead, this,\n                     [this, machineName, propertyName, node](const QOpcUa::NodeAttributes &amp;attributes) {\n                         if (node-&gt;valueAttributeError() != QOpcUa::UaStatusCode::Good)\n                             qWarning() &lt;&lt; &quot;Failed to read&quot; &lt;&lt; machineName &lt;&lt; &quot;=&gt;&quot;\n                                        &lt;&lt; propertyName &lt;&lt; &quot;with error&quot; &lt;&lt; node-&gt;valueAttributeError();\n                         else\n                             qDebug() &lt;&lt; &quot;Value of&quot; &lt;&lt; machineName &lt;&lt; &quot;=&gt;&quot; &lt;&lt; propertyName\n                                      &lt;&lt; &quot;is&quot; &lt;&lt; node-&gt;valueAttribute();\n\n                         if (--m_pendingNodes == 0)\n                             emit done();\n                     });\n\n    if (!node-&gt;readValueAttribute()) {\n        qWarning() &lt;&lt; &quot;Failed to dispatch read request for&quot;\n                   &lt;&lt; machineName &lt;&lt; &quot;=&gt;&quot; &lt;&lt; propertyName;\n        return;\n    }\n\n    ++m_pendingNodes;\n}\n\n#include &quot;main.moc&quot;\n\nint main(int argc, char *argv[])\n{\n    QCoreApplication a(argc, argv);\n\n    OpcUaDemo demo;\n    QObject::connect(&amp;demo, &amp;OpcUaDemo::done, &amp;a, &amp;QCoreApplication::quit);\n\n    return demo.init() ? a.exec() : EXIT_FAILURE;\n} <\/code><\/pre><script>\nif (!document.getElementById('syntaxed-prism')) {\n\tvar my_awesome_script = document.createElement('script');\n\tmy_awesome_script.setAttribute('src','https:\/\/www.basyskom.de\/wp-content\/plugins\/syntax-highlighter-for-elementor\/assets\/prism2.js');\n\tmy_awesome_script.setAttribute('id','syntaxed-prism');\n\tdocument.body.appendChild(my_awesome_script);\n} else {\n\twindow.Prism && Prism.highlightAll();\n}\n<\/script>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-4c1890a elementor-widget elementor-widget-text-editor\" data-id=\"4c1890a\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<p>As expected, the two woodworking machines are found, their requested property NodeIds are printed along with their name and reading the value attributes returns the expected values.<\/p>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-68ca420 elementor-widget elementor-widget-elementor-syntax-highlighter\" data-id=\"68ca420\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"elementor-syntax-highlighter.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<pre><code class='language-solid'>Found woodworking machine &quot;BasicWoodworking&quot; &quot;ns=19;i=66382&quot;\nFound woodworking machine &quot;FullWoodworking&quot; &quot;ns=20;i=66382&quot;\nNode &quot;BasicWoodworking&quot; =&gt; &quot;Manufacturer&quot; has NodeId &quot;ns=19;i=59137&quot;\nNode &quot;BasicWoodworking&quot; =&gt; &quot;SerialNumber&quot; has NodeId &quot;ns=19;i=59132&quot;\nNode &quot;BasicWoodworking&quot; =&gt; &quot;LocationGPS&quot; was not found\nNode &quot;BasicWoodworking&quot; =&gt; &quot;YearOfConstruction&quot; has NodeId &quot;ns=19;i=59135&quot;\nNode &quot;FullWoodworking&quot; =&gt; &quot;Manufacturer&quot; has NodeId &quot;ns=20;i=59160&quot;\nNode &quot;FullWoodworking&quot; =&gt; &quot;SerialNumber&quot; has NodeId &quot;ns=20;i=59155&quot;\nNode &quot;FullWoodworking&quot; =&gt; &quot;LocationGPS&quot; has NodeId &quot;ns=20;i=59167&quot;\nNode &quot;FullWoodworking&quot; =&gt; &quot;YearOfConstruction&quot; has NodeId &quot;ns=20;i=59158&quot;\nValue of &quot;BasicWoodworking&quot; =&gt; &quot;Manufacturer&quot; is QVariant(QOpcUaLocalizedText, QOpcUaLocalizedText(&quot;&quot;, &quot;Michael Weinig AG&quot;))\nValue of &quot;BasicWoodworking&quot; =&gt; &quot;SerialNumber&quot; is QVariant(QString, &quot;123456&quot;)\nValue of &quot;BasicWoodworking&quot; =&gt; &quot;YearOfConstruction&quot; is QVariant(ushort, 2021)\nValue of &quot;FullWoodworking&quot; =&gt; &quot;Manufacturer&quot; is QVariant(QOpcUaLocalizedText, QOpcUaLocalizedText(&quot;&quot;, &quot;Michael Weinig AG&quot;))\nValue of &quot;FullWoodworking&quot; =&gt; &quot;SerialNumber&quot; is QVariant(QString, &quot;123456&quot;)\nValue of &quot;FullWoodworking&quot; =&gt; &quot;LocationGPS&quot; is QVariant(QString, &quot;49.628661 9.654903&quot;)\nValue of &quot;FullWoodworking&quot; =&gt; &quot;YearOfConstruction&quot; is QVariant(ushort, 2021) <\/code><\/pre><script>\nif (!document.getElementById('syntaxed-prism')) {\n\tvar my_awesome_script = document.createElement('script');\n\tmy_awesome_script.setAttribute('src','https:\/\/www.basyskom.de\/wp-content\/plugins\/syntax-highlighter-for-elementor\/assets\/prism2.js');\n\tmy_awesome_script.setAttribute('id','syntaxed-prism');\n\tdocument.body.appendChild(my_awesome_script);\n} else {\n\twindow.Prism && Prism.highlightAll();\n}\n<\/script>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-49924bb elementor-widget elementor-widget-heading\" data-id=\"49924bb\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"heading.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<h2 class=\"elementor-heading-title elementor-size-default\">Generalizing the Approach<\/h2>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-2a5a38c elementor-widget elementor-widget-text-editor\" data-id=\"2a5a38c\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<p><span style=\"text-align: var(--text-align); color: var( --e-global-color-f7d9691 );\">This minimal example can be extended to support deeper hierarchies by simply extending the <\/span><i style=\"text-align: var(--text-align); color: var( --e-global-color-f7d9691 );\">BrowsePath<\/i><span style=\"text-align: var(--text-align); color: var( --e-global-color-f7d9691 );\"> with more elements. Depending on the intended architecture and complexity of the model, it might also be a good idea to build a tree of classes that represent OPC UA objects and resolve only the direct child nodes, which might be in turn represented by another custom object class depending on the child&#8217;s type.<\/span><\/p><p>For any <i>MandatoryPlaceholder<\/i> or <i>OptionalPlaceholder<\/i>\u00a0child nodes of an object type where no fixed BrowseName is specified, a browse call similar to the one that was used to find the woodworking machines in the\u00a0<i>Machines<\/i>\u00a0folder can be employed using a start node resolved via\u00a0<em>TranslateBrowsePathsToNodeIds<\/em>.<\/p><p>Some manufacturers who implement an OPC UA companion specification will create their own subtypes of object types like <i>WwMachineType<\/i> to add company-specific additional information. In order to deal with such a server in a generic way, it is necessary to recursively browse the <i>ObjectTypes<\/i> hierarchy first to identify the underlying supported object type from the specification. The client implementation will then just ignore any custom additions and treat the object according to the known type.<\/p><p>The demonstrated approach can be implemented with any decent OPC UA client SDK.<\/p>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-abe6386 elementor-widget elementor-widget-heading\" data-id=\"abe6386\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"heading.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<h2 class=\"elementor-heading-title elementor-size-default\">Conclusion<\/h2>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-9159c2c elementor-widget elementor-widget-text-editor\" data-id=\"9159c2c\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<p>With the help of some namespace URIs and well-known NodeIds defined by companion specifications, the demo code has avoided all use of hardcoded NodeIds not defined by the standard.<\/p><p>The approach works for any official companion specification or\u00a0<span style=\"text-align: var(--text-align); color: var( --e-global-color-f7d9691 );\">custom OPC UA model<\/span><span style=\"text-align: var(--text-align); color: var( --e-global-color-f7d9691 );\">. If implemented correctly, it will never fail, no matter what OPC UA server is on the other side, provided it conforms to the model.<\/span><\/p>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>OPC UA client code that relies on hardcoded NodeIds is brittle and often only works with a specific OPC UA server instance. This article shows the proper way to write robust and portable OPC UA client code.<\/p>","protected":false},"author":4,"featured_media":4445,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"inline_featured_image":false,"footnotes":""},"categories":[1,2,781,230,8],"tags":[],"class_list":["post-9419","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-allgemein","category-blog","category-newsletter-2024-02","category-opc-ua","category-qt"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.basyskom.de\/en\/wp-json\/wp\/v2\/posts\/9419","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.basyskom.de\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.basyskom.de\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.basyskom.de\/en\/wp-json\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/www.basyskom.de\/en\/wp-json\/wp\/v2\/comments?post=9419"}],"version-history":[{"count":342,"href":"https:\/\/www.basyskom.de\/en\/wp-json\/wp\/v2\/posts\/9419\/revisions"}],"predecessor-version":[{"id":11176,"href":"https:\/\/www.basyskom.de\/en\/wp-json\/wp\/v2\/posts\/9419\/revisions\/11176"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.basyskom.de\/en\/wp-json\/wp\/v2\/media\/4445"}],"wp:attachment":[{"href":"https:\/\/www.basyskom.de\/en\/wp-json\/wp\/v2\/media?parent=9419"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.basyskom.de\/en\/wp-json\/wp\/v2\/categories?post=9419"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.basyskom.de\/en\/wp-json\/wp\/v2\/tags?post=9419"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}