UNPKG

node-red-contrib-cip-st-ethernet-ip

Version:

A Node-RED node to interact with Allen Bradley / Rockwell PLCs using the EtherNet/IP Protocol

860 lines (760 loc) 51.6 kB
<!-- Copyright: (c) 2016-2020, ST-One Ltda., Guilherme Francescon Cittolin <guilherme@st-one.io> GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) --> <style> .eth-ip-vars-prog-title { /*min-height: 35px;*/ background-color: #f3f3f3; padding: 5px; } .eth-ip-vars-prog-handle { padding: 5px; color: #ddd; } .eth-ip-vars-prog-chevron { margin: 3px 5px; width: 10px; } .node-input-variables-container-row, .node-input-variables-container-row li { padding: 0; } input.eth-ip-vars-var-name { width: 40% !important; } select.eth-ip-vars-var-type { width: 20% !important; margin-left: 5px; } input.eth-ip-vars-var-mapping { width: 30% !important; margin-left: 5px; } .node-input-variables-container-row .red-ui-editableList-container { padding: 0; min-height: 300px; } .eth-ip-vars-prog-item .red-ui-editableList-container { border-radius: 0; border: none; height: auto !important; min-height: unset; } .eth-ip-vars-var-list { padding: 0 7px; } .plc-list { border:1px solid lightgrey; border-radius: 4px; padding: 5px; } .plc-list-tag { min-height: 300px; } .plc-list-item { list-style-type: none; } .plc-list-item>a { color: blue; } </style> <script type="text/html" data-template-name="eth-ip endpoint"> <div class="form-row"> <ul style="background: #fff; min-width: 600px; margin-bottom: 20px;" id="node-config-ethip-endpoint-tabs"></ul> </div> <div id="node-config-ethip-endpoint-tabs-content" style="min-height: 170px; height: 95%"> <div id="ethip-endpoint-tab-connection" style="display:none"> <div class="form-row"> <label for="node-config-input-address"><i class="fa fa-globe"></i> <span data-i18n="ethip.endpoint.label.address"></span></label> <input class="input-append-left" type="text" id="node-config-input-address" data-i18n="[placeholder]ethip.endpoint.label.address" style="width: 40%;"> <label for="node-config-input-slot" style="margin-left: 10px; width: 35px; "> <span data-i18n="ethip.endpoint.label.slot"></span></label> <input type="text" id="node-config-input-slot" data-i18n="[placeholder]ethip.endpoint.label.port" style="width: 50px"> </div> <div class="form-row"> <label for="node-config-input-cycletime"><i class="fa fa-refresh"></i> <span data-i18n="ethip.endpoint.label.cycletime"></span></label> <input type="text" id="node-config-input-cycletime" data-i18n="[placeholder]ethip.endpoint.label.cycletime" style="width: 60px;"> <span>ms</span> </div> <div class="form-row"> <label for="node-config-input-timeout"><i class="fa fa-refresh"></i> <span data-i18n="ethip.endpoint.label.timeout"></span></label> <input type="text" id="node-config-input-timeout" data-i18n="[placeholder]ethip.endpoint.label.timeout" style="width: 60px;"> <span>ms</span> </div> <div class="form-row"> <label for="node-config-input-name"><i class="fa fa-tag"></i> <span data-i18n="ethip.label.name"></span></label> <input type="text" id="node-config-input-name" data-i18n="[placeholder]ethip.label.name"> </div> <div class="form-row"> <label style="width: 40%;" for="node-config-input-connectedMess"><i class="fa fa-exchange"></i> <span data-i18n="ethip.endpoint.label.connectedMess"></span></label> <input type="checkbox" id="node-config-input-connectedMess" style="width: 20px; vertical-align: middle;"> </div> <p><b>PLC Browser</b></p> <div class="form-row plc-list"> <ul id="node-config-browser-container"></ul> </div> </div> <div id="ethip-endpoint-tab-variables" style="display:none; height: 90%;"> <div class="form-row eth-ip-vars-button-group" style="margin-bottom:0; height: 5%"> <a href="#" class="editor-button editor-button-small eth-ip-vars-btn-clean" style="margin: 4px; float: right;"><i class="fa fa-trash-o"></i> <span data-i18n="ethip.endpoint.label.variables.clean"></span></a> <a href="#" class="editor-button editor-button-small eth-ip-vars-btn-add-prog" style="margin: 4px; float: right;"><i class="fa fa-plus"></i> <span data-i18n="ethip.endpoint.label.variables.add"></span></a> <a href="#" class="editor-button editor-button-small eth-ip-vars-btn-expand" style="margin: 4px; float: right;"><i class="fa fa-angle-double-down"></i></a> <a href="#" class="editor-button editor-button-small eth-ip-vars-btn-collapse" style="margin: 4px; float: right;"><i class="fa fa-angle-double-up"></i></a> <label><b><span data-i18n="ethip.endpoint.label.variables.list"></b></span></label> </div> <div class="form-row node-input-variables-container-row" style="margin-bottom: 0px; max-height: 100%; overflow-y: auto"> <ol id="node-config-input-variables-container" style="min-height: 250px; min-width: 450px;"></ol> </div> <div class="form-row"> <a href="#" class="editor-button editor-button-small eth-ip-vars-btn-export" style="margin: 4px; float: right"><i class="fa fa-download"></i> <span data-i18n="ethip.endpoint.label.variables.export"></span></a> <input type="file" id="node-config-ethip-endpoint-var-import" style="display: none"/> <a href="#" class="editor-button editor-button-small eth-ip-vars-btn-import" style="margin: 4px; float: right"><i class="fa fa-upload"></i> <span data-i18n="ethip.endpoint.label.variables.import"></span></a> </div> <p><b>Tag Browser</b></p> <div class="form-row node-config-browser-tags-container-row" style="margin-bottom:0;"> <div id="node-config-browser-tags-container" style="min-height: 150px;border: 1px solid #ccc;border-radius: 5px;overflow: scroll;"> <ul id="node-config-browser-tags"><li>Click On PLC Browser Address To Update List</li></ul> </div> </div> </div> </div> </script> <script type="text/javascript"> (function(){ var saveAs = saveAs || function (e) { "use strict"; if (typeof e === "undefined" || typeof navigator !== "undefined" && /MSIE [1-9]\./.test(navigator.userAgent)) { return } var t = e.document, n = function () { return e.URL || e.webkitURL || e }, r = t.createElementNS("http://www.w3.org/1999/xhtml", "a"), o = "download" in r, a = function (e) { var t = new MouseEvent("click"); e.dispatchEvent(t) }, i = /constructor/i.test(e.HTMLElement) || e.safari, f = /CriOS\/[\d]+/.test(navigator.userAgent), u = function (t) { (e.setImmediate || e.setTimeout)(function () { throw t }, 0) }, s = "application/octet-stream", d = 1e3 * 40, c = function (e) { var t = function () { if (typeof e === "string") { n().revokeObjectURL(e) } else { e.remove() } }; setTimeout(t, d) }, l = function (e, t, n) { t = [].concat(t); var r = t.length; while (r--) { var o = e["on" + t[r]]; if (typeof o === "function") { try { o.call(e, n || e) } catch (a) { u(a) } } } }, p = function (e) { if (/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(e.type)) { return new Blob([String.fromCharCode(65279), e], { type: e.type }) } return e }, v = function (t, u, d) { if (!d) { t = p(t) } var v = this, w = t.type, m = w === s, y, h = function () { l(v, "writestart progress write writeend".split(" ")) }, S = function () { if ((f || m && i) && e.FileReader) { var r = new FileReader; r.onloadend = function () { var t = f ? r.result : r.result.replace(/^data:[^;]*;/, "data:attachment/file;"); var n = e.open(t, "_blank"); if (!n) e.location.href = t; t = undefined; v.readyState = v.DONE; h() }; r.readAsDataURL(t); v.readyState = v.INIT; return } if (!y) { y = n().createObjectURL(t) } if (m) { e.location.href = y } else { var o = e.open(y, "_blank"); if (!o) { e.location.href = y } } v.readyState = v.DONE; h(); c(y) }; v.readyState = v.INIT; if (o) { y = n().createObjectURL(t); setTimeout(function () { r.href = y; r.download = u; a(r); h(); c(y); v.readyState = v.DONE }); return } S() }, w = v.prototype, m = function (e, t, n) { return new v(e, t || e.name || "download", n) }; if (typeof navigator !== "undefined" && navigator.msSaveOrOpenBlob) { return function (e, t, n) { t = t || e.name || "download"; if (!n) { e = p(e) } return navigator.msSaveOrOpenBlob(e, t) } } w.abort = function () { }; w.readyState = w.INIT = 0; w.WRITING = 1; w.DONE = 2; w.error = w.onwritestart = w.onprogress = w.onwrite = w.onabort = w.onerror = w.onwriteend = null; return m }(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content); if (typeof module !== "undefined" && module.exports) { module.exports.saveAs = saveAs } else if (typeof define !== "undefined" && define !== null && define.amd !== null) { define("FileSaver.js", function () { return saveAs }) } //["BOOL", "SINT", "INT", "DINT", "LINT", "USINT", "UINT", "UDINT", "REAL", "LREAL", "STIME", "DATE", "TIME_AND_DAY", "DATE_AND_STRING", "STRING", "WORD", "DWORD", "BIT_STRING", "LWORD", "STRING2", "FTIME", "LTIME", "ITIME", "STRINGN", "SHORT_STRING", "TIME", "EPATH", "ENGUNIT", "STRINGI", "STRUCT"]; var dataTypes = ["", "BOOL", "SINT", "INT", "DINT", "REAL", "STRING", "STRUCT", "LINT"]; function debuglog(){ //console.log(arguments); } function generateVarTable(){ var vartable = {}; var progItems = $("#node-config-input-variables-container").editableList('items'); progItems.each(function (i){ var progItem = $(this); var progName = progItem.find('.eth-ip-vars-prog-name').val() || ''; vartable[progName] = {}; var varItems = progItem.find('.eth-ip-vars-var-list').editableList('items'); varItems.each(function (j){ var varItem = $(this); var varName = varItem.find('.eth-ip-vars-var-name').val(); var varType = varItem.find('.eth-ip-vars-var-type').val(); var varMapping = varItem.find('.eth-ip-vars-var-mapping').val(); vartable[progName][varName] = { type: varType, mapping: varMapping }; }); }); debuglog('generateVarTable', vartable); return vartable; } RED.nodes.registerType('eth-ip endpoint', { category: 'config', defaults: { address: { value: "", validate: RED.validators.regex(/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/) }, slot: { value: "0", validate: RED.validators.number() }, cycletime: { value: 3000 }, timeout: { value: 10000 }, name: { value: "" }, connectedMess: { value: true }, vartable: { value: { '': { '': { type: '', mapping: '' } } } } }, label: function() { return this.name || this.address + ":" + this.slot; }, oneditprepare: function() { var self = this; //labels (i18n) var labelName = this._("ethip.endpoint.label.variables.name"); var labelAddr = this._("ethip.endpoint.label.variables.addr"); var labelAdd = this._("ethip.endpoint.label.variables.add"); var labelDel = this._("ethip.endpoint.label.variables.del"); var labelGlobal = this._("ethip.endpoint.label.variables.global"); var labelProgram = this._("ethip.endpoint.label.variables.program"); var labelTag = this._("ethip.endpoint.label.variables.tag"); var labelMap = this._("ethip.endpoint.label.variables.mapping"); var labelConnectedMess = this._("ethip.endpoint.label.variables.connectedMess"); //elms var progContainer = $("#node-config-input-variables-container"); var btnCollapse = $('.eth-ip-vars-btn-collapse'); var btnExpand = $('.eth-ip-vars-btn-expand'); var btnAddProg = $('.eth-ip-vars-btn-add-prog'); var btnClean = $('.eth-ip-vars-btn-clean'); var btnImport = $('.eth-ip-vars-btn-import'); var btnExport = $('.eth-ip-vars-btn-export'); var browser = $("#node-config-browser-container"); var tagList = $("#node-config-browser-tags"); // Prepare tabs var tabs = RED.tabs.create({ id: "node-config-ethip-endpoint-tabs", onchange: function(tab) { $("#node-config-ethip-endpoint-tabs-content").children().hide(); $("#" + tab.id).show(); } }); tabs.addTab({ id: "ethip-endpoint-tab-connection", label: this._("ethip.endpoint.label.tabs.connection") }); tabs.addTab({ id: "ethip-endpoint-tab-variables", label: this._("ethip.endpoint.label.tabs.variables") }); setTimeout(function() { tabs.resize() }, 0); function cleanTable(skipGlobal){ progContainer.empty(); if(skipGlobal) return; progContainer.editableList('addItem', {name: '', isGlobal: true, vars: {}}); } // export function exportCSV() { var vars = generateVarTable(); var lines = []; Object.keys(vars).forEach(function (progName){ Object.keys(vars[progName]).forEach(function (varName) { var obj = vars[progName][varName]; var line = [progName, varName, obj.type, obj.mapping]; lines.push(line.join(';')); }); }); saveAs(new Blob([lines.join('\r\n')]), 'ethip-endpoint' + (self.name ? '_' + self.name : '') + '.csv'); } // import function importCSV(e) { var file = e.target.files[0]; if (!file) { return; } var reader = new FileReader(); reader.onload = function (e) { var res = {}, i, fields, resKeys; var contents = e.target.result || ''; var lines = contents.split(/[\r\n]+/); if (!lines.length) { alert('file is empty!'); return; } debuglog('importCSV lines', lines); for (i = 0; i < lines.length; i++) { lines[i] = lines[i].trim(); if (lines[i] == '') continue; fields = lines[i].split(/[\t;]/); if (fields.length < 2) { alert('line must have at least two parameters, program and tag name'); return; } res[fields[0]] = res[fields[0]] || {}; res[fields[0]][fields[1]] = { type: fields[2], mapping: fields[3] || '' } } debuglog('importCSV result', res) resKeys = Object.keys(res); if (resKeys.length) { cleanTable(true); resKeys.forEach(function(elm){ progContainer.editableList('addItem', {name: elm, isGlobal: elm === '', vars: res[elm]}); }); } }; reader.readAsText(file); } // Buttons btnCollapse.click(function(evt) { //TODO replace with the right selectors progContainer.find(".eth-ip-vars-var-container").slideUp(); progContainer.find(".eth-ip-vars-prog-title>.eth-ip-vars-prog-chevron").css({ "transform": "rotate(-90deg)" }); evt.preventDefault(); }); btnExpand.click(function(evt) { //TODO replace with the right selectors progContainer.find(".eth-ip-vars-var-container").slideDown(); progContainer.find(".eth-ip-vars-prog-title>.eth-ip-vars-prog-chevron").css({ "transform": "" }); evt.preventDefault(); }); btnAddProg.click(function(evt) { progContainer.editableList('addItem', {}); evt.preventDefault(); }); btnClean.click(function(evt) { cleanTable(); evt.preventDefault(); }); btnExport.click(exportCSV); btnImport.click(function(evt) { $('#node-config-ethip-endpoint-var-import').click(); }); $('#node-config-ethip-endpoint-var-import').on('change', importCSV); // toggle slide tab group content var titleToggle = function (content, chevron) { return function (evt) { if (content.is(":visible")) { content.slideUp(); chevron.css({ "transform": "rotate(-90deg)" }); //content.addClass('nr-db-sb-collapsed'); } else { content.slideDown(); chevron.css({ "transform": "" }); //content.removeClass('nr-db-sb-collapsed'); } }; }; progContainer.editableList({ sortable: ".eth-ip-vars-prog-handle", //removable: "eth-ip-vars-prog-btn-del", removable: false, addButton: false, addItem: function (container, i, opts){ container.addClass('eth-ip-vars-prog-item'); var titleRow = $('<div/>', {class:"eth-ip-vars-prog-title"}).appendTo(container); var handle = $('<i class="fa fa-bars eth-ip-vars-prog-handle"/>').appendTo(titleRow); var chevron = $('<i class="fa fa-angle-down eth-ip-vars-prog-chevron"/>').appendTo(titleRow); var progName; if(opts.isGlobal){ progName = $('<span style=""/>').text(labelGlobal).appendTo(titleRow); } else { progName = $('<input/>',{type: "text", placeholder: labelProgram, class: "eth-ip-vars-prog-name"}).appendTo(titleRow); progName.val(opts.name); } var buttonGroup = $('<div/>', {style:"float: right"}).appendTo(titleRow); var buttonAddVar = $('<a href="#" style="margin: auto 4px" class="editor-button editor-button-small"><i class="fa fa-plus"></i> ' + labelAdd + '</a>').appendTo(buttonGroup); var buttonDelProg = $('<a href="#" style="margin: auto 4px" class="editor-button editor-button-small eth-ip-vars-prog-btn-del"><i class="fa fa-remove"></i></a>').appendTo(buttonGroup); if(opts.isGlobal){ buttonDelProg.hide(); } //-- var contentRow = $('<div/>', {class:"eth-ip-vars-var-container"}).appendTo(container); var ol = $('<ol>', {class:"eth-ip-vars-var-list"}).appendTo(contentRow).editableList({ addButton: false, height: 'auto', connectWith: '.eth-ip-vars-var-list', removable: true, sortable: true, addItem: function (container, i, opts) { container.addClass('eth-ip-vars-var-item'); var varName = $('<input/>', {type:"text", placeholder: labelTag, class:"eth-ip-vars-var-name"}).appendTo(container); var varType = $('<select/>', {class: "eth-ip-vars-var-type"}).appendTo(container); for (var i in dataTypes) { $('<option></option>').val(dataTypes[i]).text(dataTypes[i]).appendTo(varType); } var varMapping = $('<input/>', {type:"text", placeholder: labelMap, class:"eth-ip-vars-var-mapping"}).appendTo(container); varName.val(opts.name); varType.val(opts.data ? opts.data.type : undefined); varMapping.val(opts.data ? opts.data.mapping : undefined); } }); //-- buttonAddVar.click(function (evt) { ol.editableList('addItem', {}); evt.stopPropagation(); evt.preventDefault(); }); buttonDelProg.click(function (evt) { progContainer.editableList('removeItem', container.data('data')); evt.stopPropagation(); evt.preventDefault(); }); chevron.click(titleToggle(contentRow, chevron)); Object.keys(opts.vars || {}).forEach(function (elm) { ol.editableList('addItem', {name: elm, data: opts.vars[elm]}); }); } }); Object.keys(self.vartable).forEach(function (elm) { progContainer.editableList('addItem', {name: elm, isGlobal: elm === '', vars: self.vartable[elm]}); }) // ---------------------------------------- $("#node-config-input-cycletime").spinner({ min: 50 }); $("#node-config-input-timeout").spinner({ min: 1000 }); if (self.timeout === undefined) { self.timeout = 10000; $("#node-config-input-timeout").val(10000); } // PLC Browser $.getJSON('eth-ip',function(data) { data.forEach(plc => { browser.append('<li class="plc-list-item"><a href="#">' + plc.socketAddress.sin_addr + '</a> : ' + plc.productName + '</li>') }) browser.on('click', 'li a', function() { $('#node-config-input-address').val($(this).html()) let params = {plcAddress: $(this).html()} $.post('eth-ip-tag', params, function(data) { tagList.html("") data.forEach(tag => { tagList.append('<li class="plc-list-item"><a href="#">' + (tag.program ? tag.program + ":" : "") + tag.name + '</a> type: <span class="tag-type">' + tag.type.typeName +'</span></li>') }) tagList.on('click', 'li a', function() { self.vartable = generateVarTable(); let tagArray = $(this).html().split(':'); let tagType = $(this).next('span').html(); let prgName = ""; let tagName = ""; let tagTypes = { ASCIISTRING82: 'STRING', STRING: 'STRING', STRUCT: 'STRUCT', BOOL: 'BOOL', INT: 'INT', SINT: 'SINT', DINT: 'DINT', LINT: 'LINT', REAL: 'REAL' }; if(!Object.keys(tagTypes).includes(tagType)) {tagType = 'STRUCT'} if(tagArray.length > 1) { prgName = tagArray[0]; tagName = tagArray[1]; } else { tagName = tagArray[0]; } self.vartable[prgName][tagName] = {type: tagTypes[tagType]}; progContainer.editableList('empty') Object.keys(self.vartable).forEach(function (elm) { progContainer.editableList('addItem', {name: elm, isGlobal: elm === '', vars: self.vartable[elm]}); }); }); }); }); }); }, oneditsave: function() { var node = this; node.vartable = generateVarTable(); } }); })(); </script> <!-- ######################################################################################## --> <script type="text/html" data-template-name="eth-ip in"> <div class="form-row"> <label for="node-input-endpoint"><i class="fa fa-bolt"></i> <span data-i18n="ethip.in.label.endpoint"></span></label> <input type="text" id="node-input-endpoint" data-i18n="[placeholder]ethip.in.label.endpoint"> </div> <div class="form-row"> <label for="node-input-mode"><i class="fa fa-sliders"></i> <span data-i18n="ethip.in.label.mode"></span></label> <select type="text" id="node-input-mode"> <option value="single" data-i18n="ethip.in.mode.single"></option> <option value="all-split" data-i18n="ethip.in.mode.all-split"></option> <option value="all" data-i18n="ethip.in.mode.all"></option> </select> </div> <div class="form-row ethip-input-var-row"> <label for="node-input-program"><i class="fa fa-random"></i> <span data-i18n="ethip.in.label.program"></span></label> <select type="text" id="node-input-program"> </select> </div> <div class="form-row ethip-input-var-row"> <label for="node-input-variable"><i class="fa fa-random"></i> <span data-i18n="ethip.in.label.variable"></span></label> <select type="text" id="node-input-variable"> </select> </div> <div class="form-row"> <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="ethip.label.name"></span></label> <input type="text" id="node-input-name" data-i18n="[placeholder]ethip.label.name"> </div> <div class="form-row"> <label style="width: 60%;" for="node-input-gatherMetrics"><i class="fa fa-file"></i> <span data-i18n="ethip.in.label.gatherMetrics"></span></label> <input type="checkbox" id="node-input-gatherMetrics" style="width: 20px; vertical-align: middle;"> </div> <div class="form-row"> <label style="width: 60%;" for="node-input-includeTimestamp"><i class="fa fa-clock-o"></i> <span data-i18n="ethip.in.label.includeTimestamp"></span></label> <input type="checkbox" id="node-input-includeTimestamp" style="width: 20px; vertical-align: middle;"> </div> <div class="form-row"> <label style="width: 60%;" for="node-input-addErrorOutput"><i class="fa fa-bug"></i> <span data-i18n="ethip.in.label.addErrorOutput"></span></label> <input type="checkbox" id="node-input-addErrorOutput" style="width: 20px; vertical-align: middle;"> <input type="hidden" id="node-input-outputs" value="1"> </div> </script> <script type="text/javascript"> RED.nodes.registerType('eth-ip in', { category: 'plc', defaults: { endpoint: { value: "", type: "eth-ip endpoint", required: true }, mode: { value: "single" }, variable: { value: "", validate: function (value) { return this.mode == "single" ? !!value : true; } }, program: { value: "" }, name: { value: "" }, gatherMetrics: { value: false }, includeTimestamp: { value: false }, addErrorOutput: { value: false }, outputs: { value: 1 } }, color: "#D19BA1", inputs: 0, outputs: 1, icon: "serial.png", label: function() { if (this.name) return this.name; var endpointNode = RED.nodes.node(this.endpoint); if (endpointNode) { if (this.mode != 'single') { return endpointNode.label(); } if (this.program) { return this.program + " / " + this.variable; } return this.variable || endpointNode.label(); } return this._("ethip.in.label.name"); }, labelStyle: function() { return this.name ? "node_label_italic" : ""; }, oneditprepare: function () { var self = this; var progList = $('#node-input-program'); var varList = $('#node-input-variable'); var modeList = $('#node-input-mode'); var endpointList = $("#node-input-endpoint"); var varDivs = $('.ethip-input-var-row'); var errorOutput = $('#node-input-addErrorOutput'); var vars = {}; function cleanupLists(cleanProg) { $('#node-input-variable option').remove(); varList.append($('<option/>', { disabled: "disabled", selected: "selected", style: "display:none;", text: self._("ethip.in.label.variable-select"), value: "" })); if (!cleanProg) return; $('#node-input-program option').remove(); } function updateProgList(endpointId) { //cleanup selects cleanupLists(true); var endpointNode = RED.nodes.node(endpointId); if (!endpointNode) return; vars = endpointNode.vartable || {}; Object.keys(vars).forEach(function (elm) { var txt = elm === "" ? self._("ethip.out.label.global") : elm; progList.append($('<option/>', { text: txt, value: elm })); if (elm === self.program) { progList.val(self.program); updateVarList(elm); } }); } function updateVarList(progName) { cleanupLists(false); if(!vars[progName]) return; Object.keys(vars[progName]).forEach(function (elm) { varList.append($('<option/>', { value: elm, text: elm })); if (elm == self.variable) { varList.val(self.variable); } }); } progList.change(function () { updateVarList(progList.val()); }); endpointList.change(function () { updateProgList(endpointList.val()); }); updateProgList(self.endpoint); if (this.outputs == 2) { errorOutput.prop('checked', true); } else { errorOutput.prop('checked', false); } errorOutput.change(function () { if (errorOutput.is(":checked")) { $('#node-input-outputs').val(2); } else { $('#node-input-outputs').val(1); } }); modeList.change(function () { if (modeList.val() == "single") { varDivs.show(); } else { varDivs.hide(); } }); modeList.change(); }, oneditsave: function() { var endpointNode = RED.nodes.node($("#node-input-endpoint").val()); var progValue = $('#node-input-program').val(); var varValue = $('#node-input-variable').val(); var gatherMetricsValue = $('#node-input-gatherMetrics').val(); var includeTimestampValue = $('#node-input-includeTimestamp').val(); var addErrorOutputValue = $('#node-input-addErrorOutput').val(); if(!endpointNode) return; //validate: cleanup fields if they don't exist on the endpoint if(!endpointNode.vartable[progValue]){ this.program = ""; this.variable = ""; } else if (!endpointNode.vartable[progValue][varValue]) { this.variable = ""; } endpointNode.gatherMetrics = gatherMetricsValue; endpointNode.includeTimestamp = includeTimestampValue; endpointNode.addErrorOutput = addErrorOutputValue; } }); </script> <!-- ######################################################################################## --> <script type="text/html" data-template-name="eth-ip out"> <div class="form-row"> <label for="node-input-endpoint"><i class="fa fa-bolt"></i> <span data-i18n="ethip.out.label.endpoint"></span></label> <input type="text" id="node-input-endpoint" data-i18n="[placeholder]ethip.out.label.endpoint"> </div> <div class="form-row"> <label for="node-input-program"><i class="fa fa-random"></i> <span data-i18n="ethip.out.label.program"></span></label> <select type="text" id="node-input-program"> </select> </div> <div class="form-row"> <label for="node-input-variable"><i class="fa fa-random"></i> <span data-i18n="ethip.out.label.variable"></span></label> <select type="text" id="node-input-variable"> </select> </div> <div class="form-row"> <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="ethip.label.name"></span></label> <input type="text" id="node-input-name" data-i18n="[placeholder]ethip.label.name"> </div> <div class="form-tips"><span data-i18n="[html]ethip.out.disclaimer"></span></div> </script> <script type="text/javascript"> RED.nodes.registerType('eth-ip out', { category: 'plc', defaults: { endpoint: { value: "", type: "eth-ip endpoint", required: true }, variable: { value: "", //validate: function(value) { this.program ? this.value : true } }, program: { value: "" }, name: { value: "" } }, color: "#D19BA1", inputs: 1, outputs: 0, icon: "serial.png", align: 'right', label: function() { if (this.name) return this.name; var endpointNode = RED.nodes.node(this.endpoint); if (endpointNode) { if (this.program) { return this.program + " / " + this.variable; } return this.variable || endpointNode.label(); } return this._("ethip.out.label.name"); }, labelStyle: function() { return this.name ? "node_label_italic" : ""; }, oneditprepare: function() { var self = this; var progList = $('#node-input-program'); var varList = $('#node-input-variable'); var endpointList = $("#node-input-endpoint"); var vars = {}; function cleanupLists(cleanProg){ $('#node-input-variable option').remove(); varList.append($('<option/>', { disabled: "disabled", selected: "selected", style: "display:none;", text: self._("ethip.out.label.variable-select"), value: "" })); if(!cleanProg) return; $('#node-input-program option').remove(); } function updateProgList(endpointId) { //cleanup selects cleanupLists(true); var endpointNode = RED.nodes.node(endpointId); if (!endpointNode) return; vars = endpointNode.vartable || {}; Object.keys(vars).forEach(function (elm) { var txt = elm === "" ? self._("ethip.out.label.global") : elm; progList.append($('<option/>', { text: txt, value: elm })); if(elm === self.program){ progList.val(self.program); updateVarList(elm); } }); } function updateVarList(progName) { cleanupLists(false); if (progList.val() == "") { varList.append($('<option/>', { value: "", text: "" })); } if(!vars[progName]) return; Object.keys(vars[progName]).forEach(function (elm){