Injecting standard companion specifications (RFID example)
Goal
Load a standard companion specification (RFID) into your scripted server and instantiate a complex object type. The pattern generalises to any OPC UA companion spec: AutoID, DI, Machinery, ISA-95, and so on.
Prerequisites
- The basic scripted server is working — see Creating an OPC UA server.
- The
nodesets:field accepts the built-in names that ship with the palette —"standard","di","autoId","machinery","ia", ... — or absolute paths to your own.NodeSet2.xmlfiles. Use the latter when you have a downloaded companion spec that isn't bundled.
The Boot Function
const sterfive = global.get("sterfive");
if (!sterfive) {
node.error("global.get('sterfive') is not set — is the Sterfive OPC UA palette loaded?");
} else {
const { bootstrap, opcua } = sterfive;
const { bootstrapServer } = bootstrap;
const cfg = msg.config || {};
const handle = await bootstrapServer({
port: cfg.port ?? 4840,
endpoint: cfg.endpoint || "node-red-server",
nodesets: ["standard", "di", "autoId"],
onPopulate: (addressSpace, exposed) => {
const namespace = addressSpace.getOwnNamespace();
const nsAutoId = addressSpace.getNamespaceIndex("http://opcfoundation.org/UA/AutoID/");
const rfidReaderDeviceType = addressSpace.findObjectType("RfidReaderDeviceType", nsAutoId);
if (!rfidReaderDeviceType) {
throw new Error("RfidReaderDeviceType not found — is the AutoID nodeset loaded?");
}
const devicesFolder = namespace.addFolder("ObjectsFolder", { browseName: "Devices" });
const rfidDevice = rfidReaderDeviceType.instantiate({
organizedBy: devicesFolder,
browseName: "RFID_Reader_1",
nodeId: "s=RFID_Reader_1",
optionals: ["DeviceHealth", "DeviceStatus", "ScanActive", "LastScanData"],
});
const deviceStatus = rfidDevice.getComponentByName("DeviceStatus", nsAutoId);
if (deviceStatus) {
deviceStatus.writeEnumValue("Idle");
exposed.deviceStatus = deviceStatus;
}
const scanActive = rfidDevice.getComponentByName("ScanActive", nsAutoId);
if (scanActive) {
scanActive.setValueFromSource({ dataType: opcua.DataType.Boolean, value: false });
exposed.scanActive = scanActive;
}
},
});
flow.set("$rfid_deviceStatus", handle.exposed.deviceStatus);
flow.set("$rfid_scanActive", handle.exposed.scanActive);
flow.set("$opcuaHandle", handle);
node.send({ payload: `OPC UA Server running at ${handle.server.getEndpointUrl()}` });
}
If you have an XML file you've downloaded yourself, mix paths and built-ins:
nodesets: [
"standard",
"/srv/nodesets/Opc.Ua.Di.NodeSet2.xml",
"/srv/nodesets/Opc.Ua.AutoID.NodeSet2.xml",
],
Paths must be absolute. The bootstrap helper validates each entry up-front and throws a helpful error listing built-in names if you misspell one — that pre-empts node-opcua's [NODE-OPCUA-E02] generateAddressSpace : cannot find nodeset2 xml file crash, which would otherwise leave the server in a partially-initialized state.
Nodesets are construction-time only — nodesets: is part of the config-identity hash. Adding a new nodeset to the boot Function and redeploying triggers an automatic teardown-then-rebuild.
Update values dynamically
In a separate Update Function:
const sterfive = global.get("sterfive");
if (!sterfive) {
node.error("global.get('sterfive') is not set — is the Sterfive OPC UA palette loaded?");
} else {
const { opcua } = sterfive;
const scanActive = flow.get("$rfid_scanActive");
if (!scanActive) {
node.warn("server not booted");
} else {
scanActive.setValueFromSource({
dataType: opcua.DataType.Boolean,
value: Boolean(msg.payload),
});
}
}
Tips
- The companion specs define many optional components.
optionals: [...]controls which ones are materialised on instantiation. Be deliberate; instantiating everything inflates the address space. getComponentByName(name, namespaceIndex)requires the namespace index because the same browse name might exist in several namespaces.- For composite extension-object values (e.g.,
RfidScanResult), useaddressSpace.constructExtensionObject(dataTypeNode, fields)to build the value, thensetValueFromSource({ dataType: DataType.ExtensionObject, value: extObj }). - Several companion specs depend on others (AutoID requires DI, MachineTool requires Machinery, etc.). Load the prerequisites in
nodesets:order.
Further reading
For more tips and examples, see the Sterfive book node-opcua by example.