node-red-opcua-x
Version:
<img src="images/node-red-logo.png" alt="drawing" style="height:200px;"/> <!-- <img src="icons/opcua-logo.png" alt="drawing" style="height:200px;"/> -->
377 lines (305 loc) • 11.2 kB
JavaScript
const { EventEmitter } = require('events')
const {
OPCUAClient,
ClientSubscription,
OPCUAServer,
NodeId,
Variant,
StatusCodes,
DataType,
UAObject
} = require('node-opcua')
/**@type {OPCUAClient[]} */
let opcClients = [];
/** @type {OPCUAServer} */
let opcServer = null;
/**@type {EventEmitter} */
const eventEmitter = new EventEmitter();
function CreateOpcUaClient(connectionId, name, authOption) {
const connectionStrategy = {
initialDelay: 1000,
maxDelay: 10000,
maxRetry: 100
};
const client = opcClients[connectionId];
if (client) return client;
const newClient = OPCUAClient.create({
applicationName: name,
keepSessionAlive: true,
keepAliveInterval: 1000,
connectionStrategy: connectionStrategy,
securityMode: authOption.securityMode,
securityPolicy: authOption.securityPolicy,
endpointMustExist: false
});
opcClients[connectionId] = newClient;
return newClient;
}
async function Connect(connectionId, host, userOption) {
const client = opcClients[connectionId];
if (!client) return;
_attachClientListeners(client);
try {
await client.connect(host);
const session = await client.createSession(userOption);
_attachSessionListeners(session);
client['session'] = session;
CreateSubscription(connectionId, session);
}
catch (err) {
console.error(err.message);
return;
}
}
async function Close(connectionId) {
let client = opcClients[connectionId];
if (!client) return;
try {
await _closeSession(client);
_removeClientListeners(client);
if (!client.isReconnecting) {
client.disconnect();
}
client = null;
opcClients[connectionId] = null;
} catch (err) {
console.error(err.message);
}
}
async function CloseSubscription(session) {
let subscription = session.subscription;
if (!subscription) return;
await subscription.terminate();
subscription = null;
}
function CreateSubscription(connectionId, session) {
if (session.subscription) return session.subscription;
const subscriptionOptions = {
requestedPublishingInterval: 1000,
requestedLifetimeCount: 100,
requestedMaxKeepAliveCount: 10,
maxNotificationsPerPublish: 100,
publishingEnabled: true,
priority: 10
};
const subscription = ClientSubscription.create(session, subscriptionOptions);
session['subscription'] = subscription;
eventEmitter.emit('subscription_created', connectionId);
return subscription;
}
function IsValidNodeId(nodeId) {
const regex = /^(ns=\d+;)?(i=\d+|s=[\w\-]+|g=[\da-fA-F\-]+|b=[\da-fA-F]*)$/i;
return regex.test(nodeId);
}
function GetClient(connectionId) {
return opcClients[connectionId];
}
function CreateOpcUaServer() {
opcServer = new OPCUAServer({
port: 4334,
resourcePath: "/UA/node-red-x",
buildInfo: {
productName: "MySimulatedNodeRedXServer",
buildNumber: "7658",
buildDate: new Date(2025, 3, 11)
}
});
}
async function InitializeServer() {
await opcServer.initialize();
const addressSpace = opcServer.engine.addressSpace;
const namespace = addressSpace.getOwnNamespace();
// declare a new object
const simNode = namespace.addObject({
organizedBy: addressSpace.rootFolder.objects,
browseName: "SIM"
});
const scalarFolder = namespace.addObject({
organizedBy: simNode,
browseName: "Scalar",
typeDefinition: "FolderType"
});
//Scalar values
_addVariable(namespace, scalarFolder, DataType[DataType.Boolean], DataType.Boolean);
_addVariable(namespace, scalarFolder, DataType[DataType.SByte], DataType.SByte);
_addVariable(namespace, scalarFolder, DataType[DataType.Byte], DataType.Byte);
_addVariable(namespace, scalarFolder, DataType[DataType.Int16], DataType.Int16);
_addVariable(namespace, scalarFolder, DataType[DataType.UInt16], DataType.UInt16);
_addVariable(namespace, scalarFolder, DataType[DataType.Int32], DataType.Int32);
_addVariable(namespace, scalarFolder, DataType[DataType.UInt32], DataType.UInt32);
_addVariable(namespace, scalarFolder, DataType[DataType.Int64], DataType.Int64);
_addVariable(namespace, scalarFolder, DataType[DataType.UInt64], DataType.UInt64);
_addVariable(namespace, scalarFolder, DataType[DataType.Float], DataType.Float);
_addVariable(namespace, scalarFolder, DataType[DataType.Double], DataType.Double);
_addVariable(namespace, scalarFolder, DataType[DataType.String], DataType.String);
_addVariable(namespace, scalarFolder, DataType[DataType.DateTime], DataType.DateTime);
_addVariable(namespace, scalarFolder, DataType[DataType.Guid], DataType.Guid);
_addVariable(namespace, scalarFolder, DataType[DataType.ByteString], DataType.ByteString);
_addVariable(namespace, scalarFolder, DataType[DataType.XmlElement], DataType.XmlElement);
_addVariable(namespace, scalarFolder, DataType[DataType.LocalizedText], DataType.LocalizedText);
_addVariable(namespace, scalarFolder, DataType[DataType.QualifiedName], DataType.QualifiedName);
_addVariable(namespace, scalarFolder, DataType[DataType.NodeId], DataType.NodeId);
await opcServer.start();
}
async function CloseServer() {
await opcServer.shutdown(1000);
}
function GetServer() { return opcServer; }
//#region private
async function _closeSession(client) {
let session = client.session;
if (!session) return;
_removeSessionListeners(session);
await CloseSubscription(session);
await session.close();
session = null;
}
function _attachClientListeners(client) {
client.on("connected", () => _notifyClientState(client));
client.on("abort", () => _notifyClientState(client));
client.on("close", () => _notifyClientState(client));
client.on("connection_reestablished", () => _notifyClientState(client));
client.on("connection_lost", () => _notifyClientState(client));
client.on("start_reconnection", () => _notifyClientState(client));
}
function _removeClientListeners(client) {
client.removeListener("connected", () => _notifyClientState(client));
client.removeListener("abort", () => _notifyClientState(client));
client.removeListener("close", () => _notifyClientState(client));
client.removeListener("connection_reestablished", () => _notifyClientState(client));
client.removeListener("connection_lost", () => _notifyClientState(client));
client.removeListener("start_reconnection", () => _notifyClientState(client));
}
function _attachSessionListeners(session) {
session.on("session_restored", () => _updateSessionState(session, "restored"));
session.on("session_closed", () => _updateSessionState(session, "closed"));
session.on("keepalive", () => _updateSessionState(session, "keepalive"));
session.on("keepalive_failure", () => _updateSessionState(session, "keepalive_failed"));
}
function _removeSessionListeners(session) {
session.removeListener("session_restored", () => _updateSessionState(session, "restored"));
session.removeListener("session_closed", () => _updateSessionState(session, "closed"));
session.removeListener("keepalive", () => _updateSessionState(session, "keepalive"));
session.removeListener("keepalive_failure", () => _updateSessionState(session, "keepalive_failed"));
}
function _notifyClientState(client) {
// console.debug(client.applicationName + ": " + client._internalState);
eventEmitter.emit('client_state', client);
}
function _updateSessionState(session, state) {
// switch (state) {
// case "restored":
// console.debug(session.sessionId + ": session restored");
// break
// case "closed":
// console.debug(session.sessionId + ": session closed");
// break;
// case "keepalive":
// console.debug(session.sessionId + ": session keepalive");
// break;
// case "keepalive_failed":
// console.debug(session.sessionId + ": session keepalive failed");
// break;
// }
eventEmitter.emit('session_state', session, state);
}
/**
* Adds a variable to the OPC UA namespace.
*
* @param {Object} namespace - The namespace object to which the variable will be added.
* @param {import('node-opcua').UAObject} parentNode - The parent node under which the variable will be added.
* @param {string} browseName - The browse name of the variable.
* @param {DataType} dataType - The data type of the variable.
*/
function _addVariable(namespace, parentNode, browseName, dataType) {
namespace.addVariable({
componentOf: parentNode,
browseName: browseName,
dataType: dataType,
value: {
get: () => {
return new Variant({
dataType: dataType,
//arrayType: VariantArrayType.Scalar,
value: _getDefaultValue(dataType)
});
},
set: () => {
return StatusCodes.Good;
}
}
});
}
/**
*
* @param {DataType} dataType - The data type of the variable.
*/
function _getDefaultValue(dataType) {
let defaultValue;
switch (dataType) {
case DataType.Double:
case DataType.Float:
defaultValue = 0.0;
break;
case DataType.Int16:
case DataType.Int32:
case DataType.UInt16:
case DataType.UInt32:
case DataType.Byte:
case DataType.SByte:
defaultValue = 0;
break;
case DataType.Int64:
case DataType.UInt64:
defaultValue = Number(0n);
break;
case DataType.String:
defaultValue = "";
break;
case DataType.Boolean:
defaultValue = false;
break;
case DataType.DateTime:
defaultValue = new Date();
break;
case DataType.Guid:
defaultValue = "8cae8c9a-a1d6-4e93-b680-d9a20c5a703c";
break;
case DataType.ByteString:
defaultValue = Buffer.alloc(0);
break;
case DataType.XmlElement:
defaultValue = "<xml></xml>";
break;
case DataType.LocalizedText:
defaultValue = { text: "", locale: "" };
break;
case DataType.QualifiedName:
defaultValue = { name: "", namespaceIndex: 0 };
break;
case DataType.NodeId:
defaultValue = new NodeId();
break;
default:
defaultValue = null;
break;
}
return defaultValue;
}
//#endregion
//#region export
module.exports = {
CreateOpcUaClient,
Connect,
Close,
CloseSubscription,
CreateSubscription,
GetClient,
IsValidNodeId,
CreateOpcUaServer,
CloseServer,
InitializeServer,
GetServer,
eventEmitter
}
//#endregion