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
- 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
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.