node-red-contrib-iiot-opcua
Version:
An Industrial IoT OPC UA toolbox contribution package for Node-RED based on node-opcua.
307 lines (304 loc) • 11.9 kB
JavaScript
/**
The BSD 3-Clause License
Copyright 2022 - DATATRONiQ GmbH (https://datatroniq.com)
Copyright (c) 2018-2022 Klaus Landsdorf (http://node-red.plus/)
Copyright 2015,2016 - Mika Karaila, Valmet Automation Inc. (node-red-contrib-opcua)
All rights reserved.
node-red-contrib-iiot-opcua
*/
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const opcua_iiot_core_1 = require("./opcua-iiot-core");
const node_opcua_1 = require("node-opcua");
const debug_1 = __importDefault(require("debug"));
const underscore_1 = require("underscore");
const internalDebugLog = (0, debug_1.default)('opcuaIIoT:browser'); // eslint-disable-line no-use-before-define
const detailDebugLog = (0, debug_1.default)('opcuaIIoT:browser:details'); // eslint-disable-line no-use-before-define
const crawlerInternalDebugLog = (0, debug_1.default)('opcuaIIoT:browser:crawler'); // eslint-disable-line no-use-before-define
const crawlerDetailDebugLog = (0, debug_1.default)('opcuaIIoT:browser:crawler:details'); // eslint-disable-line no-use-before-define
const browse = (session, nodeIdToBrowse) => {
return new Promise(function (resolve, reject) {
let browseOptions = [
{
nodeId: nodeIdToBrowse,
referenceTypeId: 'Organizes',
includeSubtypes: true,
browseDirection: node_opcua_1.BrowseDirection.Forward,
resultMask: 63
},
{
nodeId: nodeIdToBrowse,
referenceTypeId: 'Aggregates',
includeSubtypes: true,
browseDirection: node_opcua_1.BrowseDirection.Forward,
resultMask: 63
}
];
session.browse(browseOptions, (err, browseResult) => {
if (err) {
reject(err);
}
else {
if ((0, underscore_1.isUndefined)(browseResult)) {
reject('Browse Results are Undefined');
}
else {
resolve(browseResult);
}
}
});
});
};
const browseAddressSpaceItems = function (session, addressSpaceItems) {
return new Promise(function (resolve, reject) {
let browseOptions = [];
addressSpaceItems.flatMap(function (item) {
browseOptions.push({
nodeId: item.nodeId,
referenceTypeId: 'Organizes',
includeSubtypes: true,
browseDirection: node_opcua_1.BrowseDirection.Forward,
resultMask: 63
});
browseOptions.push({
nodeId: item.nodeId,
referenceTypeId: 'Aggregates',
includeSubtypes: true,
browseDirection: node_opcua_1.BrowseDirection.Forward,
resultMask: 63
});
});
if (browseOptions.length === 0) {
resolve([]);
return;
}
session.browse(browseOptions, (err, browseResult) => {
if (err) {
reject(err);
}
else {
if ((0, underscore_1.isUndefined)(browseResult)) {
reject('Browse Results are Undefined');
}
else {
resolve(browseResult);
}
}
});
});
};
const createCrawler = function (session) {
return new node_opcua_1.NodeCrawler(session);
};
const crawl = (session, nodeIdToCrawl, msg, sendWrapper) => {
if (!nodeIdToCrawl) {
return new Error('NodeId To Crawl Not Valid');
}
const message = Object.assign({}, msg);
const crawler = createCrawler(session);
let crawlerResult = [];
const data = {
onBrowse: function (crawler, cacheNode) {
crawlerResult.push(cacheNode);
node_opcua_1.NodeCrawler.follow(crawler, cacheNode, this);
}
};
try {
const crawlback = (err) => {
if (err) {
sendWrapper(err);
}
else {
sendWrapper({
crawlerResult,
rootNodeId: nodeIdToCrawl
});
}
};
const readCallback = (err, response) => {
if (err) {
sendWrapper(err);
}
else if (response && response.some((res) => { var _a; return ((_a = res.statusCode) === null || _a === void 0 ? void 0 : _a.name) === 'BadNodeIdUnknown'; })) {
sendWrapper(new Error('NodeId Not Valid: Please enter a valid NodeId, under the "OPC UA Nodes" tab of the Inject Node configuration.'));
}
else {
crawler.crawl(nodeIdToCrawl, data, crawlback);
}
};
verifyNodeExists(session, nodeIdToCrawl, readCallback);
}
catch (err) {
return err;
}
};
/**
* Crawl based on addressSpaceItems from an inject node.
*
* This seems needlessly overcomplicated, but that is necessary to avoid UnhandledPromiseRejection errors from the crawl function.
*
*/
const crawlAddressSpaceItems = (session, payload, sendWrapper, timeout) => {
const crawler = createCrawler(session);
const crawlerPromises = [];
const resolvers = [];
payload.addressSpaceItems.forEach((item, index) => {
if (!item.nodeId) {
coreBrowser.internalDebugLog('Item Not To Crawl - Missing NodeId');
return;
}
// Each item should track results itself
// results will be combined in the payload.value field, but remain independent in payload.crawlerResult
let crawlerResult = [];
const data = {
onBrowse: (crawler, cacheNode, userData) => {
if (!cacheNode) {
coreBrowser.internalDebugLog('Item Not To Crawl - Missing NodeId');
}
crawlerResult.push(cacheNode);
node_opcua_1.NodeCrawler.follow(crawler, cacheNode, userData);
}
};
/**
* Handle the response of the verifyNodeExists function.
* If the response doesn't contain any error, then the node exists and it can be crawled..
*/
const readCallback = (err, response) => {
crawlerPromises.push(new Promise((resolve, reject) => {
resolvers.push({ resolve, reject });
setTimeout(reject, timeout * 1000, new Error('Timeout'));
}).catch((test) => test)); // The catch needs to be here, despite seeming useless
/**
* Resolves the promise of the current item.
* If the current item is the last, wait for all promises to resolve, then call the send function.
* Intended as a callback for the crawl function, but also called directly, since this needs to be called every time.
*/
const crawlback = (err) => {
resolvers[index].resolve(crawlerResult);
// Ensure only one message is sent
if (index === payload.addressSpaceItems.length - 1) {
Promise.allSettled(crawlerPromises).then((promiseList) => {
sendWrapper({ rootNodeId: item.nodeId, payload, crawlerResult: promiseList, promises: true });
}).catch((err) => {
sendWrapper(err);
});
}
};
// Crawlback must be called in every branch to ensure a message is sent
if (err) {
resolvers[index].reject(err);
crawlback(err);
return;
}
else if (response && response.some((res) => { var _a; return ((_a = res.statusCode) === null || _a === void 0 ? void 0 : _a.name) === 'BadNodeIdUnknown'; })) {
const error = new Error('NodeId Not Valid: Please enter a valid NodeId, under the "OPC UA Nodes" tab of the Inject Node configuration.');
resolvers[index].reject(error);
crawlback(error);
return;
}
crawler.crawl(item.nodeId, data, crawlback);
};
verifyNodeExists(session, item.nodeId, readCallback);
});
};
const browseToRoot = function () {
detailDebugLog('Browse To Root ' + opcua_iiot_core_1.OBJECTS_ROOT);
return opcua_iiot_core_1.OBJECTS_ROOT;
};
const extractNodeIdFromTopic = function (payload, node) {
let rootNodeId = null;
if (payload.actiontype === 'browse') { // event driven browsing
if (payload.root && payload.root.nodeId) {
internalDebugLog('Root Selected External ' + payload.root);
rootNodeId = payload.root.nodeId;
}
else {
rootNodeId = node.nodeId;
}
detailDebugLog('Extracted NodeId ' + rootNodeId);
rootNodeId = rootNodeId || browseToRoot();
}
return rootNodeId;
};
const transformToEntry = (reference) => {
if (reference) {
try {
return reference.toJSON();
}
catch (err) {
internalDebugLog(err);
if (reference.referenceTypeId) {
return {
referenceTypeId: reference.referenceTypeId.toString(),
isForward: reference.isForward,
nodeId: reference.nodeId.toString(),
browseName: reference.browseName.toString(),
displayName: reference.displayName.toString(),
nodeClass: reference.nodeClass.toString(),
typeDefinition: reference.typeDefinition.toString()
};
}
}
}
else {
internalDebugLog('Empty Reference On Browse');
}
return reference;
};
const initBrowserNode = function () {
return {
browseTopic: opcua_iiot_core_1.OBJECTS_ROOT,
iiot: Object.assign(Object.assign({}, (0, opcua_iiot_core_1.initCoreNode)()), { items: [], messageList: [], delayMessageTimer: [] })
};
};
const browseErrorHandling = function (node, err, msg, lists, errorHandler, statusHandler, oldStatusParameter = undefined, showErrors = true, showStatusActivities = true) {
let results = (lists === null || lists === void 0 ? void 0 : lists.browserResults) || [];
if (err) {
internalDebugLog(typeof node + 'Error ' + err);
if (showErrors) {
errorHandler(err, msg);
}
if ((0, opcua_iiot_core_1.isSessionBad)(err)) {
node.emit('opcua_client_not_ready');
}
}
else {
internalDebugLog(typeof node + ' Done With Error');
if (results.length) {
detailDebugLog(results.length + 'items in lists of browser results');
}
}
if (showStatusActivities && oldStatusParameter) {
node.oldStatusParameter = (0, opcua_iiot_core_1.setNodeStatusTo)(node, 'error', oldStatusParameter, showStatusActivities, statusHandler);
}
};
/**
* Verifies that a node exists and catches the error, sending a nonexistent node to the crawler causes an error
*
* The endCallback function should do error checking and then call the crawl function
*/
const verifyNodeExists = (session, nodeId, endCallback) => {
session.read([{ nodeId: nodeId }], endCallback);
};
const coreBrowser = {
internalDebugLog,
detailDebugLog,
crawlerInternalDebugLog,
crawlerDetailDebugLog,
// Browser functions
browse,
browseAddressSpaceItems,
createCrawler,
crawl,
crawlAddressSpaceItems,
browseToRoot,
extractNodeIdFromTopic,
transformToEntry,
initBrowserNode,
browseErrorHandling,
};
exports.default = coreBrowser;
//# sourceMappingURL=opcua-iiot-core-browser.js.map