UNPKG

node-red-contrib-iiot-opcua

Version:

An Industrial IoT OPC UA toolbox contribution package for Node-RED based on node-opcua.

466 lines (407 loc) 23.7 kB
<!-- 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 --> <script type='text/javascript'> RED.nodes.registerType('OPCUA-IIoT-Server', { category: 'IIoT', color: '#ABCDEF', defaults: { port: {value: '55388', required: true}, endpoint: {value: ''}, acceptExternalCommands: {value: true}, maxAllowedSessionNumber: {value: ''}, maxConnectionsPerEndpoint: {value: ''}, maxAllowedSubscriptionNumber: {value: ''}, alternateHostname: {value: ''}, name: {value: ''}, showStatusActivities: {value: false}, showErrors: {value: false}, asoDemo: {value: true}, allowAnonymous: {value: true}, individualCerts: {value: false}, isAuditing: {value: false}, serverDiscovery: {value: true}, users: { value: [] }, xmlsets: { value: [] }, publicCertificateFile: {value: ''}, privateCertificateFile: {value: ''}, registerServerMethod: {value: 1}, discoveryServerEndpointUrl: {value: ''}, capabilitiesForMDNS: {value: ''}, maxNodesPerRead: {value: 1000, validate:function(v) { return ((v === '') || (RED.validators.number(v) && (v > 0) && (v <= 100000))) }}, maxNodesPerBrowse: {value: 2000, validate:function(v) { return ((v === '') || (RED.validators.number(v) && (v > 0) && (v <= 200000))) }}, delayToClose: {value: 1000, validate:function(v) { return ((v === '') || (RED.validators.number(v) && (v >= 100) && (v <= 15000))) }} }, inputs: 1, outputs: 1, align: 'right', icon: 'icon.png', label: function () { return this.name || 'Demo-Server' }, labelStyle: function () { return this.name ? 'node_label_italic' : '' }, oneditprepare: function () { let node = this node.xmlsetSelect = [] let cacheItemCount = 0 let cacheItemCountSet = 0 let tabs = RED.tabs.create({ id: "node-input-server-tabs", onchange: function (tab) { $("#node-input-server-tabs-content").children().hide() $("#" + tab.id).show() } }) tabs.addTab({ id: "opcuaiiot-server-tab-settings", label: this._("opcua-iiot-contrib.tabs-label.settings") }) tabs.addTab({ id: "opcuaiiot-server-tab-limits", label: this._("opcua-iiot-contrib.tabs-label.limits") }) tabs.addTab({ id: "opcuaiiot-server-tab-security", label: this._("opcua-iiot-contrib.tabs-label.security") }) tabs.addTab({ id: "opcuaiiot-server-tab-users", label: this._("opcua-iiot-contrib.tabs-label.users") }) tabs.addTab({ id: "opcuaiiot-server-tab-discovery", label: this._("opcua-iiot-contrib.tabs-label.discovery") }) // User Management if(node.users && node.users.length > 0) { cacheItemCount = node.users.length node.users.forEach(function(element, index, array) { generateUserEntry(element, index); }) } // XML-Set Management if(node.xmlsets && node.xmlsets.length > 0) { cacheItemCountSet = node.xmlsets.length node.xmlsets.forEach(function(element, index, array) { generateXMLSetEntry(element, index); }) } function generateUserEntry(user, id) { let container = $('<li/>', {style: "background: #fefefe; margin:0; padding:8px 0px; border-bottom: 1px solid #ccc;"}) let row = $('<div id="row' + id + '"/>').appendTo(container) $('<i style="color: #eee; cursor: move;" class="node-input-server-users-handle fa fa-bars"></i>').appendTo(row) let userField = $('<input/>', { id: "node-input-server-users-name" + id, class: 'opcuaUserName', type: "text", style: "margin-left:5px;width:160px;", placeholder: 'name' }).appendTo(row) let passwordField = $('<input/>', { id: "node-input-server-users-password" + id, class: 'opcuaUserPassword', type: "password", style: "margin: 0 auto;width:55%;min-width:60px;margin-left:5px", placeholder: 'password' }).appendTo(row) userField.val(user.name) passwordField.val(user.password) let finalspan = $('<span/>', {style: "float: right;margin-right: 10px;"}).appendTo(row) let removeUserButton = $('<a/>', { href: "#", id: 'node-button-user-remove' + id, class: "editor-button editor-button-small", style: "margin-top: 7px; margin-left: 5px;" }).appendTo(finalspan) $('<i/>', {class: "fa fa-remove"}).appendTo(removeUserButton) removeUserButton.click(function () { container.css({"background": "#fee"}) container.fadeOut(300, function () { $(this).remove() }); }); $("#node-input-server-users-container").append(container) } function generateXMLSetEntry(xmlset, id) { let container = $('<li/>', {style: "background: #fefefe; margin:0; padding:8px 0px; border-bottom: 1px solid #ccc;"}) let row = $('<div id="xmlsetRow' + id + '"/>').appendTo(container) $('<i style="color: #eee; cursor: move;" class="node-input-server-xmlsets-handle fa fa-bars"></i>').appendTo(row) let nameField = $('<input/>', { id: "node-input-server-xmlsets-name" + id, type: "text", class: 'xmlsetName', style: "margin-left:5px;width:75px;", placeholder: 'name' }).appendTo(row) let pathField = $('<input/>', { id: "node-input-server-xmlsets-path" + id, class: 'xmlsetPath', type: "text", style: "margin: 0 auto;width:68%;min-width:60px;margin-left:5px", placeholder: 'public/vendor/opc-foundation/xml/Opc.ISA95.NodeSet2.xml' }).appendTo(row) nameField.val(xmlset.name) pathField.val(xmlset.path) let finalspan = $('<span/>', {style: "float: right;margin-right: 10px;"}).appendTo(row) let lookupItemButton = $('<a/>', { href: '#', id: 'node-button-xmlsets-lookup' + id, class: 'editor-button editor-button-small', style: 'margin-top: 7px; margin-left: 5px;' }).appendTo(finalspan) lookupItemButton.click(function () { let xmlsetLookupButton = $('#node-button-xmlsets-lookup' + id) let xmlsetLookupField = $('#xmlsetRow' + id + ' #node-input-server-xmlsets-path' + id) xmlsetLookupButton.addClass('disabled') $.getJSON('/opcuaIIoT/xmlsets/public', function (data) { xmlsetLookupButton.removeClass('disabled') node.xmlsetSelect = [] $.each(data, function (i, filename) { node.xmlsetSelect.push(filename) }) xmlsetLookupField.autocomplete({ source: node.xmlsetSelect, minLength: 0, close: function (event, ui) { xmlsetLookupField.autocomplete('destroy') } }).autocomplete('search', '') }) }) $('<i/>', {class: 'fa fa-search'}).appendTo(lookupItemButton) let removeXMLSetButton = $('<a/>', { href: "#", id: 'node-button-xmlset-remove' + id, class: "editor-button editor-button-small", style: "margin-top:7px;margin-left:5px;" }).appendTo(finalspan) $('<i/>', {class: "fa fa-remove"}).appendTo(removeXMLSetButton) removeXMLSetButton.click(function () { container.css({"background": "#fee"}) container.fadeOut(300, function () { $(this).remove() }); }); $("#node-input-server-xmlsets-container").append(container) } $("#node-input-server-users-container").sortable({ axis: "y", handle: ".node-input-server-users-handle", cursor: "move" }); $("#node-input-server-xmlsets-container").sortable({ axis: "y", handle: ".node-input-server-xmlsets-handle", cursor: "move" }); $("#node-input-server-users-container .node-input-server-users-handle").disableSelection() $("#node-input-server-xmlsets-container .node-input-server-xmlsets-handle").disableSelection() $("#node-input-server-users-add").click(function () { if(!cacheItemCount || cacheItemCount < 0) { cacheItemCount = 0 } generateUserEntry({ name: '', password: '' }, cacheItemCount++) // length is every time one more than index $("#node-input-server-users-container-div").scrollTop($("#node-input-server-users-container-div").get(0).scrollHeight) }) $("#node-input-server-xmlsets-add").click(function () { if(!cacheItemCountSet || cacheItemCountSet < 0) { cacheItemCountSet = 0 } generateXMLSetEntry({ name: '', path: '' }, cacheItemCountSet++) // length is every time one more than index $("#node-input-server-xmlsets-container-div").scrollTop($("#node-input-server-xmlsets-container-div").get(0).scrollHeight) }) function switchDialogResize() { switchUserDialogResize() switchXMLSetDialogSetResize() } // dialog User handling function switchUserDialogResize() { let rows = $("#dialog-form>div:not(.node-input-server-users-container-row)") let height = $("#dialog-form").height() rows.each(function (index, row) { height -= row.outerHeight(true) }) let editorRow = $("#dialog-form>div.node-input-server-users-container-row") height -= (parseInt(editorRow.css("marginTop")) + parseInt(editorRow.css("marginBottom"))) $("#node-input-server-users-container-div").css("height", height + "px") } // dialog XML-Set handling function switchXMLSetDialogSetResize() { let rows = $("#dialogSet-form>div:not(.node-input-server-xmlsets-container-row)") let height = $("#dialogSet-form").height() rows.each(function (index, row) { height -= row.outerHeight(true) }) let editorRow = $("#dialogSet-form>div.node-input-server-xmlsets-container-row") height -= (parseInt(editorRow.css("marginTop")) + parseInt(editorRow.css("marginBottom"))) $("#node-input-server-xmlsets-container-div").css("height", height + "px") } $("#dialog").on("dialogresize", switchDialogResize) $("#dialog").on("dialogopen", function (ev) { let size = $("#dialog").dialog('option', 'sizeCache-switch') if (size) { $("#dialog").dialog('option', 'width', size.width) $("#dialog").dialog('option', 'height', size.height) switchDialogResize() } else { setTimeout(switchDialogResize, 10) } }) $("#dialog").on("dialogclose", function (ev, ui) { $("#dialog").off("dialogresize", switchDialogResize) }) // Certificate Management try { let certsCheckbox = $("#node-input-individualCerts"); let configCertFields = $("#node-config-certFiles"); if (node.individualCerts) { certsCheckbox.prop('checked', true); configCertFields.show(); } else { certsCheckbox.prop('checked', false); configCertFields.hide(); } certsCheckbox.change(function () { if ($(this).is(":checked")) { configCertFields.show(); } else { configCertFields.hide(); $('#node-config-input-publicCertificateFile').val(''); $('#node-config-input-privateKeyFile').val(''); } }); } catch (err) { this.error(err); } }, oneditsave: function () { let node = this let cacheUsers = $("#node-input-server-users-container").children() node.users = [] cacheUsers.each(function () { node.users.push({ name: $(this).find('.opcuaUserName').val(), password: $(this).find('.opcuaUserPassword').val(), }) }) let cacheXMLSets = $('#node-input-server-xmlsets-container').children() node.xmlsets = [] cacheXMLSets.each(function () { node.xmlsets.push({ name: $(this).find('.xmlsetName').val(), path: $(this).find('.xmlsetPath').val(), }) }) // console.log('well done editsave ...') } }) </script> <script type='text/x-red' data-template-name='OPCUA-IIoT-Server'> <div class='form-row'> <ul style='background:#fff;min-width:600px;margin-bottom:20px' id='node-input-server-tabs'></ul> </div> <div id='node-input-server-tabs-content' style='min-height:170px'> <div id='opcuaiiot-server-tab-settings' style='display:none'> <div class='form-row'> <label for='node-input-port'><i class='fa fa-fort-awesom'></i> <span data-i18n='opcua-iiot-contrib.label.port'></span></label> <!-- dynamic Ports from 49152 to 65535 (c000hex bis FFFFhex) --> <input type='text' id='node-input-port' placeholder='Ports 49152 bis 65535 (c000hex bis FFFFhex)'> </div> <div class='form-row'> <label for='node-input-endpoint'><i class='icon-tasks'></i> <span data-i18n='opcua-iiot-contrib.label.endpoint'></span></label> <input type='text' id='node-input-endpoint' placeholder='UA/NodeREDIIoTServer'> </div> <div class='form-row'> <label for='node-input-alternateHostname'><i class='fa fa-server'></i> <span data-i18n='opcua-iiot-contrib.label.alternateHostname'></span></label> <input type='text' id='node-input-alternateHostname' placeholder=''> </div> <hr> <div class='form-row'> <label for='node-input-delayToClose'><i class='icon-time'></i> <span data-i18n='opcua-iiot-contrib.label.delayToClose'></span> </label> <input type='text' id='node-input-delayToClose' placeholder='1000' style='width:80px'> </div> <div class='form-row'> <label for='node-input-name'><i class='fa fa-tag'></i> <span data-i18n='node-red:common.label.name'> </span></label> <input type='text' id='node-input-name' placeholder=''> </div> <hr> <div class='form-row'> <label style='min-width:160px' for='node-input-isAuditing'><i class='fa fa-th'></i> <span data-i18n='opcua-iiot-contrib.label.isAuditing'></span></label> <input type='checkbox' id='node-input-isAuditing' style='max-width:30px'> </div> <div class='form-row'> <label style='min-width:160px' for='node-input-asoDemo'><i class='fa fa-th'></i> <span data-i18n='opcua-iiot-contrib.label.asoDemo'></span></label> <input type='checkbox' id='node-input-asoDemo' style='max-width:30px'> </div> <hr> <div class='form-row'> <label style='min-width:160px' for='node-input-showStatusActivities'><i class='fa fa-bolt'></i> <span data-i18n='opcua-iiot-contrib.label.showActivities'></span></label> <input type='checkbox' id='node-input-showStatusActivities' style='max-width:30px'> </div> <div class='form-row'> <label style='min-width:160px' for='node-input-showErrors'><i class='fa fa-exclamation-circle'></i> <span data-i18n='opcua-iiot-contrib.label.showErrors'></span></label> <input type='checkbox' id='node-input-showErrors' style='max-width:30px'> </div> </div> <div id='opcuaiiot-server-tab-limits' style='display:none'> <div class='form-row'> <label for='node-input-maxAllowedSessionNumber'><i class='icon-list'></i> <span data-i18n='opcua-iiot-contrib.label.maxAllowedSessionNumber'></span></label> <input type='text' id='node-input-maxAllowedSessionNumber' placeholder='10' style='width:80px'> </div> <div class='form-row'> <label for='node-input-maxConnectionsPerEndpoint'><i class='icon-list'></i> <span data-i18n='opcua-iiot-contrib.label.maxConnectionsPerEndpoint'></span></label> <input type='text' id='node-input-maxConnectionsPerEndpoint' placeholder='10' style='width:80px'> </div> <div class='form-row'> <label for='node-input-maxAllowedSubscriptionNumber'><i class='icon-list'></i> <span data-i18n='opcua-iiot-contrib.label.maxAllowedSubscriptionNumber'></span></label> <input type='text' id='node-input-maxAllowedSubscriptionNumber' placeholder='50' style='width:80px'> </div> <div class='form-row'> <label for='node-input-maxNodesPerRead'><i class='icon-list'></i> <span data-i18n='opcua-iiot-contrib.label.maxNodesPerRead'> </span></label> <input type='text' id='node-input-maxNodesPerRead' placeholder='1000' style='width:80px'> </div> <div class='form-row'> <label for='node-input-maxNodesPerBrowse'><i class='icon-list'></i> <span data-i18n='opcua-iiot-contrib.label.maxNodesPerBrowse'></span></label> <input type='text' id='node-input-maxNodesPerBrowse' placeholder='2000' style='width:80px'> </div> </div> <div id='opcuaiiot-server-tab-security' style='display:none'> <div class='form-row'> <!-- SecurityPolicy enum via REST --> <label for='node-input-securityPolicy' style='min-width:140px'><i class='fa fa-certificate'></i> <span data-i18n='opcua-iiot-contrib.label.securityPolicy'></span></label> <ul style='margin-left:80px'> <li>None </li><li> Basic128 </li><li>Basic192 </li><li>Basic192Rsa15 </li><li>Basic256Rsa15 </li><li>Basic256Sha256 </li><li> Aes128_Sha256_RsaOaep </li><li>Aes256_Sha256_RsaPss </li><li>PubSub_Aes128_CTR </li><li>PubSub_Aes256_CTR </li></ul> </div> <div class='form-row'> <!-- MessageSecurityMode enum via REST --> <label for='node-input-securityMode' style='min-width:140px'><i class='fa fa-key'></i> <span data-i18n='opcua-iiot-contrib.label.securityMode'></span> </label> <ul style='margin-left:80px'> <li>None</li> <li>Sign</li> <li>Sign&Encrypt</li> </ul> </div> <div class='form-row'> <label style='min-width:140px'> <i class='fa fa-certificate'></i> <span data-i18n='opcua-iiot-contrib.label.certificateFiles'></span> </label> </div> <div class='form-row'> <label style='min-width:280px'> <i class='fa fa-file'></i> <span data-i18n='opcua-iiot-contrib.label.individualCerts'></span> <input type='checkbox' id='node-input-individualCerts' style='max-width:30px'> </label> </div> <div id='node-config-certFiles'> <hr> <div class='form-row'> <label for='node-input-publicCertificateFile'><i class='fa fa-file'></i> <span data-i18n='opcua-iiot-contrib.label.publicCertificateFile'></span></label> <input type='text' id='node-input-publicCertificateFile' placeholder='./certificates/server_selfsigned_cert_2048.pem' style='min-width:480px;width:100%'> </div> <div class='form-row'> <label for='node-input-privateCertificateFile'><i class='fa fa-file'></i> <span data-i18n='opcua-iiot-contrib.label.privateCertificateFile'></span></label> <input type='text' id='node-input-privateCertificateFile' placeholder='./certificates/PKI/own/private/private_key.pem' style='min-width:480px;width:100%'> </div> <hr> </div> <div class='form-row'> <label for='node-input-allowAnonymous' style='min-width:140px'><i class='icon-lock'></i> <span data-i18n='opcua-iiot-contrib.label.allowAnonymous'></span> </label> <input type='checkbox' id='node-input-allowAnonymous' style='max-width:35px'> </div> </div> <div id='opcuaiiot-server-tab-users' style='display:none'> <h4><span data-i18n='opcua-iiot-contrib.label.users'></span></h4> <div class='form-row node-input-server-users-container-row' style='margin-bottom:0'> <div id='node-input-server-users-container-div' style='box-sizing:border-box;border-radius:5px;height:210px;padding:5px;border:1px solid #ccc;overflow-y:scroll'> <ol id='node-input-server-users-container' style='list-style-type:none;margin:0'></ol> </div> </div> <div class='form-row'> <a href='#' class='editor-button editor-button-small' id='node-input-server-users-add' style='margin-top:4px'><i class='fa fa-plus'></i> <span data-i18n='opcua-iiot-contrib.label.addButton'></span></a> </div> <h4><span data-i18n='opcua-iiot-contrib.label.xmlsets'></span></h4> <div class='form-row node-input-server-xmlsets-container-row' style='margin-bottom:0'> <div id='node-input-server-xmlsets-container-div' style='box-sizing:border-box;border-radius:5px;height:240px;padding:5px;border:1px solid #ccc;overflow-y:scroll'> <ol id='node-input-server-xmlsets-container' style='list-style-type:none;margin:0'></ol> </div> </div> <div class='form-row'> <a href='#' class='editor-button editor-button-small' id='node-input-server-xmlsets-add' style='margin-top:4px'><i class='fa fa-plus'></i> <span data-i18n='opcua-iiot-contrib.label.addButton'></span></a> </div> </div> <div id='opcuaiiot-server-tab-discovery' style='display:none'> <div class='form-row'> <!-- SecurityPolicy enum via REST --> <label for='node-input-registerServerMethod' style='min-width:160px'><i class='fa fa-discover'></i> <span data-i18n='opcua-iiot-contrib.label.registerServerMethod'></span></label> <select type='text' id='node-input-registerServerMethod'> <option value='1'>HIDDEN</option> <option value='2'>MDNS</option> <option value='3'>LDS</option> </select> </div> <div class='form-row'> <span style='min-width:160px'> <ul> <li> HIDDEN: the server doesn't expose itself to the external world </li><li> MDNS: the server publish itself to the mDNS Multicast network directly </li><li> LDS: the server registers itself to the LDS or LDS-ME (Local Discovery Server) </li></ul> </span> </div> <div class='form-row'> <label for='node-input-serverDiscovery' style='min-width:160px'><i class='fa fa-cc-discover'></i> <span data-i18n='opcua-iiot-contrib.label.serverDiscovery'></span></label> <input type='checkbox' id='node-input-serverDiscovery' style='max-width:35px'> </div> <div class='form-row'> <label for='node-input-discoveryServerEndpointUrl' style='min-width:160px'><i class='fa fa-tag'></i> <span data-i18n='opcua-iiot-contrib.label.discoveryServerEndpointUrl'></span></label> <input type='text' id='node-input-discoveryServerEndpointUrl' placeholder='opc.tcp://localhost:4840'> </div> <div class='form-row'> <label for='node-input-capabilitiesForMDNS' style='min-width:160px'><i class='fa fa-tag'></i> <span data-i18n='opcua-iiot-contrib.label.capabilitiesForMDNS'></span></label> <input type='text' id='node-input-capabilitiesForMDNS' placeholder='NA,DA,...'> </div> </div> </div> </script> <script type='text/x-red' data-help-name='OPCUA-IIoT-Server'> <h2>OPC UA IIoT Server</h2> <p> The Server node is an OPC UA server with a simple optional information model (ASO Demo). The address space is to expand with the ASO nodes and commands are to send by the Command nodes. </p> <p>Default enpoint: opc.tcp://localhost:55388</p> <p>Named enpoint: opc.tcp://localhost:55388/UA/NodeREDIIoTServer </p> <p>Default discovery: opc.tcp://localhost:4840/UADiscovery</p> <p>all discovery: opc.tcp://localhost:4840/</p> <h3> Options</h3> <p>If you need more information about all options read the node-opcua API, please!</p> <p> The 'Delay On Close' is to close the server node with delay to all other nodes.</p> <h3>Certificates</h3> <p> The server provides simple NodeOPCUA demo certificates and private keys.</p> <p> You could set up your own certificate and private key files. This is optional.</p> <h3>User</h3> <p> The user list is to set up users to the server. It is stored with Node-RED credentials best practice.</p> <h3> XML NodeSets</h3> <p>That is static for now to add ISA95, DI, and Auto-ID to a server via XML NodeSet.</p> <h3>Discovery </h3> <p>That gives you access to the servers discovery node-opcua parameters.</p> <p> Set showErrors to get errors from node-opcua on browse.</p> </script>