basysKom AnwendungsEntwicklung

Revisiting NodeOPCUA
Essential Summary
The NodeOPCUA library is a very mature OPC UA implementation for TypeScript/JavaScript on the Node.js platform. Since the initial release eleven years ago, it has been continuously updated and extended by the main developer and various external contributors. Read this article to learn if NodeOPCUA might be the right OPC UA stack for your next project.
Professionelle OPC UA Dienstleistungen

Sie benötigen eine OPC UA Schnittstelle, ein eigenes Informationsmodell oder einfach nur Beratung zum Einsatz?

Wir haben die Anforderung "OPC UA Schnittstelle" bekommen - wo fange ich an?

Zunächst muss unterschieden werden, ob ein Server oder ein Client benötigt wird. Ein OPC UA-Server ist der häufigere und auch kompliziertere Fall, da hier keine Daten auf einem vorhandenen Server konsumiert werden, sondern Daten in einem passend zu definierenden Modell bereitgestellt werden müssen.

Introducing NodeOPCUA

The NodeOPCUA library is a very mature OPC UA implementation for TypeScript/JavaScript on the Node.js platform. Since the initial release eleven years ago, it has been continuously updated and extended by the main developer and various external contributors. Read this article to learn if NodeOPCUA might be the right OPC UA stack for your next project.

Previous Experiences

When we started exploring the OPC UA protocol and its software ecosystem in 2015, NodeOPCUA was the library we used to build our first rudimentary server, which exposed data from a weather sensor in its address space. Of course we didn’t really know much back then but we were pretty impressed by how easily we could get started and quickly expose a few variable nodes using very little code.

Since then, our OPC UA journey focused on C/C++ based libraries like the now mostly abandoned freeopcua and later open62541 because they fitted much better into the software architecture of our projects. After several years of learning, launching and maintaining Qt OPC UA, making contributions to the open62541 code base, and completing OPC UA based projects for our customers, we decided to have a fresh look at NodeOPCUA based on the experience we gathered over the last years.

Our Evaluation

Our main objective was to find out if the NodeOPCUA module’s feature set would be sufficient to use it in one of our customer projects. So first of all, we made a list of the features we usually need:
  • Loading a custom OPC UA model with structured types, ObjectTypes, EventTypes and fixed objects in the Objects folder
  • All aspects of custom structured types (unions, optional fields, array fields, nested structures)
  • Exposing and connecting to endpoints with all current security policies and security modes
  • Attaching callbacks to Method and Variable nodes on the server
  • Instantiating custom object types with selected optional child nodes
  • Populating and triggering custom events on the server
  • Historizing variables on the server
  • Calling methods from the client
  • Resolving browse paths
  • Reading, writing and monitoring variables of custom structured types
  • Reading the raw history of variable nodes
  • EventNotifier monitored items with select and where clause
Then we modeled a compact OPC UA nodeset containing the types and structures mentioned above to have everything we need for our evaluation in one place. The exported nodeset XML file was then loaded into a new TypeScript based NodeOPCUA server project and after verifying the successful loading of the nodeset using UaExpert, we started implementing the mentioned features we usually require. A client adapted to interact with the custom server was implemented in a separate TypeScript project in order to evaluate the client API.

Observations and Remarks

Coming from C/C++ based OPC UA implementations, we were really impressed by the way NodeOPCUA handles structured types. The constructExtensionObject() method reduces building an extension object containing a custom structured type to just passing an object containing a property for each structure field. Just leaving out or including a property takes care of optional fields and nested structured types are deduced and constructed automatically.

const ext = await session.constructExtensionObject(coerceNodeId('ns=2;i=3003'), {
    int64ArrayMember: [ 1, 2 ],
    nestedStructArrayMember: [
        {
            doubleSubtypeMember: 23
        },
        {
            doubleSubtypeMember: 42,
        }
    ]
}) 

We also liked the makeBrowsePath() method which builds basic browse paths from a starting node and a path string like /2:MyObject/2:MyTestVariable. This covers most daily usage of browse paths, but if additional RelativePathElement fields beside the BrowseName are required, the returned BrowsePath can be easily edited manually to add the necessary information.

Building and triggering an event on the server side is also very easy. An event type and an object containing a property for each child node of the event type requiring a customized value is passed to raiseEvent() which performs all necessary steps.

const serverNode = server.engine.addressSpace?.getDefaultNamespace().findNode(ObjectIds.Server) as UAObject

if (serverNode) {
    serverNode.raiseEvent('2:MyTestEvent', {
        sourceName: { dataType: DataType.String, value: 'Server' },
        sourceNode: { dataType: DataType.NodeId, value: serverNode.nodeId },
        message: { dataType: DataType.String, value: 'Test message' },
        customDouble: { dataType: DataType.Double, value: 42 },
        customString: { dataType: DataType.String, value: 'Lorem Ipsum Dolor Sit Amet' }
    })
} 

The client side API for creating an EventNotifier monitored item is made easy by the constructEventFilter() method which takes an array of strings in the style of Message, 2:MyProperty or EnabledState.EffectiveDisplayName to build the select clause. Any additional fields of the SimpleAttributeOperand can be set in the returned EventFilter as described for ReleativePathElement. The where clause is directly built from a ContentFilterElement array.

const eventMonitoredItem = await sub.monitor({ nodeId: ObjectIds.Server, attributeId: AttributeIds.EventNotifier },
    {
        samplingInterval: 0,
        queueSize: 10, 
        filter: constructEventFilter([ 'Message', '2:CustomString', '2:CustomDouble' ])
    }, TimestampsToReturn.Both) 

The only part that seemed a bit strange from our perspective was that an HA Configuration object containing all optional child nodes is added automatically to every variable node historizing is enabled for. Using history with a variable node that is bound to a get callback (for example to get a value from an external source) was a bit of a surprise because there seems to be no built-in history sampling mechanism like in open62541. This resulted in a history that only contained values whenever a client was reading the variable or a monitored item was sampling it.

Using our knowledge of the OPC UA specification, implementing all listed points for both the client and the server side was relatively straightforward. As the examples only cover a few basics of the API and there is not much public documentation (especially for more advanced features), looking at the source code of the tests or the implementation itself was sometimes necessary to find the right way of using the NodeOPCUA module. Knowing the names of the involved OPC UA services and types definitely helped finding the corresponding code.

Conclusion

The NodeOPCUA module is not just a straight implementation of the OPC UA services but has a well thought out API with lots of great convenience methods which greatly reduce the amount of code that’s usually necessary to perform simple tasks in an OPC UA server or client.
The asynchronous nature of the OPC UA protocol and the mechanisms for asynchronous programming built into NodeJS are a perfect match. For example, chaining a TranslateBrowsePathsToNodeIds call to a read or write operation using the retrieved nodeId feels very intuitive.

With the Cyber Resilience Act looming at the horizon, it is a good idea to look into alternatives for building network facing applications in languages without manual memory management. From what we have seen in our evaluation, the NodeOPCUA module will most definitely be an option for some of our future customer projects.

Picture of Jannis Völker

Jannis Völker

Jannis Völker is a software engineer at basysKom GmbH in Darmstadt. After joining basysKom in 2017, he has been working in connectivity projects for embedded devices, Azure based cloud projects and has made contributions to Qt OPC UA and open62541. He has a background in embedded Linux, Qt and OPC UA and holds a master's degree in computer science from the University of Applied Sciences in Darmstadt.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Weitere Blogartikel

basysKom Newsletter

We collect only the data you enter in this form (no IP address or information that can be derived from it). The collected data is only used in order to send you our regular newsletters, from which you can unsubscribe at any point using the link at the bottom of each newsletter. We will retain this information until you ask us to delete it permanently. For more information about our privacy policy, read Privacy Policy