UNPKG

node-red-contrib-melsec

Version:

Node-RED Node to communicate with Mitsubishi FX over Programming Port

640 lines (553 loc) 21.4 kB
<!-- Copyright: (c) 2021, ST-One GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) --> <script type="text/html" data-template-name="melsec fx endpoint"> <div class="form-row"> <label for="node-config-input-cycletime"><i class="fa fa-refresh"></i> <span data-i18n="melsec.endpoint.label.cycletime"></span></label> <input type="text" id="node-config-input-cycletime" data-i18n="[placeholder]melsec.endpoint.label.cycletime" style="width: 60px;"> <span>ms</span> </div> <div class="form-row" style="margin-bottom:0;"> <label><i class="fa fa-list"></i> <span data-i18n="melsec.endpoint.label.variables.list"></span></label> </div> <div class="form-row node-input-variables-container-row" style="margin-bottom: 0px;"> <div id="node-config-input-variables-container-div" style="box-sizing: border-box; border-radius: 5px; height: 300px; padding: 5px; border: 1px solid #ccc; overflow-y:scroll;"> <ol id="node-config-input-variables-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-config-melsec-endpoint-var-export" style="margin: 4px; float: right"><i class="fa fa-download"></i> <span data-i18n="melsec.endpoint.label.variables.export"></span></a> <input type="file" id="node-config-melsec-endpoint-var-import" style="display: none"/> <a href="#" class="editor-button editor-button-small" id="node-config-melsec-endpoint-var-import-btn" style="margin: 4px; float: right"><i class="fa fa-upload"></i> <span data-i18n="melsec.endpoint.label.variables.import"></span></a> <a href="#" class="editor-button editor-button-small" id="node-config-input-add-variable" style="margin: 4px;"><i class="fa fa-plus"></i> <span data-i18n="melsec.endpoint.label.variables.add"></span></a> <a href="#" class="editor-button editor-button-small" id="node-config-melsec-endpoint-var-clean" style="margin: 4px;"><i class="fa fa-trash-o"></i> <span data-i18n="melsec.endpoint.label.variables.clean"></span></a> </div> <br> <div class="form-row"> <label for="node-config-input-name"><i class="fa fa-tag"></i> <span data-i18n="melsec.label.name"></span></label> <input type="text" id="node-config-input-name" data-i18n="[placeholder]melsec.label.name"> </div> </script> <script type="text/html" data-help-name="melsec fx endpoint"> <p>Configures the connection to a PLC</p> <p>This node was created by <a href="https://st-one.io" target="_blank">ST-One</a></p> <h3>Details</h3> <p> The <b>Cycle time</b> configuration specifies the time interval in which all variables will be read from the PLC. A value of <code>0</code> disables automatic reading. </p> <h3>Variable addressing</h3> <ul> <li>MEMORY AREA.</li> <ul> <li>(S,X,Y,M,D...).</li> </ul> <li>TYPE MODIFIER <strong>Can be empty if needed.</strong></li> <ul> <li>INT (Signed 16Bits).</li> <li>DINT (Signed 32Bits).</li> <li>WORD (Unsigned 16Bits).</li> <li>DWORD (Unsigned 32Bits).</li> <li>REAL (Float Single Precision 32Bits).</li> <li>REAL (Float Double Precision 64Bits).</li> </ul> <li>DEVICE OFFSET (Address Number).</li> <ul> <li>0,1,2 (Ex. D20 (20 is the device offset)).</li> </ul> <li>BIT OFFSET(.) <strong>Can be empty if needed.</strong></li> <ul> <li>D20.1 (Reads bit 1 from D20 address.)</li> </ul> <li>ARRAY LENGTH(,) <strong>Can be empty if needed.</strong></li> <ul> <li>D20,4 (Reads 4 address from D20, D21, D22 and D23).</li> </ul> </ul> <h4>Examples</h4> <ul> <li>DDINT20,4: Reads D20-D21,D22-D23,D24-D25,D26-D27</li> <li>DINT20,4: Reads D20,D21,D22,D23</li> <li>Y0</li> </ul> </script> <script type="text/javascript"> function validateMXAddress(address) { if (!address) return 'ERR_PARSE_EMPTY'; let MELSEC_REGEX_ADDR = /^([A-Z]{1})([A-Z]+)?(\d+)(?:\.(\d+))?(?:,(\d+))?$/; let MELSEC_MEM_AREA = ["S","X","Y","T","M","C","PY","OT","PM","OC","RT","RC","TV","CV16","CV32","SPECIAL_D","D"]; let MELSEC_TYPE_MODIFIER = ["INT", "DINT", "WORD", "DWORD", "REAL", "LREAL"]; let match = address.match(MELSEC_REGEX_ADDR); if (!match) return 'ERR_PARSE_UNKNOWN_FORMAT'; let match_memArea = match[1]; let match_typeModifier = match[2]; let match_device = match[3]; let match_bit = match[4]; let match_arr = match[5]; if (!MELSEC_MEM_AREA.includes(match_memArea)) return 'ERR_PARSE_MEM_AREA'; if (match_typeModifier) { if (!MELSEC_TYPE_MODIFIER.includes(match_typeModifier)) return 'ERR_PARSE_TYPE_MODIFIER'; } let deviceOffset = parseInt(match_device); if (isNaN(deviceOffset)) return 'ERR_PARSE_DEVICE_OFFSET'; if (match_bit) { let bitOffset = parseInt(match_bit); if (isNaN(bitOffset)) return 'ERR_PARSE_BIT_OFFSET'; } if (match_arr) { let arrayLength = parseInt(match_arr); if (isNaN(arrayLength)) return 'ERR_PARSE_ARRAY_LENGTH'; } return null; } function validateAddressList(list) { for (var i = 0; i < list.length; i++){ var elm = list[i]; if (!elm.name) return false; if (validateMXAddress(elm.addr)) return false; } return true; } RED.nodes.registerType('melsec fx endpoint', { category: 'config', color: '#FFAAAA', defaults: { name: { value: "" }, cycletime: { value: 1000 }, vartable: { value: [{ name: "", addr: "" }], validate: validateAddressList } }, label: function () { var self = this; if (this.name) return this.name; return "melsec fx endpoint"; }, oneditprepare: function () { var self = this; var tt = this._.bind(this); var labelName = this._("melsec.endpoint.label.variables.name"); var labelAddr = this._("melsec.endpoint.label.variables.addr"); var labelDel = this._("melsec.endpoint.label.variables.del"); $("#node-config-input-cycletime").spinner({ min: 0 }); function generateVariable(variable) { var curTooltip; var previousValue = variable.addr; var container = $('<li/>', { style: "background: #fff; margin:0; padding:8px 0px; border-bottom: 1px solid #ccc;" }); var row1 = $('<div/>').appendTo(container); var variableAddr = $('<input/>', { style: "width: 110px; margin-right: 10px;", class: "node-config-input-variable-addr", type: "text", placeholder: labelAddr }).appendTo(row1); var variableName = $('<input/>', { style: "width: 250px", class: "node-config-input-variable-name", type: "text", placeholder: labelName }).appendTo(row1); var finalspan = $('<span/>', { style: "float: right; margin-right: 10px;" }).appendTo(row1); var deleteButton = $('<a/>', { href: "#", class: "editor-button editor-button-small", style: "margin-top: 7px; margin-left: 5px;", title: labelDel }).appendTo(finalspan); $('<i/>', { class: "fa fa-remove" }).appendTo(deleteButton); deleteButton.click(function () { container.css({ "background": "#fee" }); container.fadeOut(150, function () { $(this).remove(); }); if (curTooltip) curTooltip.close(); }); variableAddr.change(function () { //validate address var curVal = variableAddr.val(); var valError = validateMXAddress(curVal); if (valError) { variableAddr.addClass('input-error') var errorText = tt("melsec.endpoint.validation." + valError); if (curTooltip) { curTooltip.setContent(errorText); curTooltip.open(); } else if (RED.popover && RED.popover.tooltip){ curTooltip = RED.popover.tooltip(variableAddr, errorText); curTooltip.open(); } } else { variableAddr.removeClass('input-error'); if(curTooltip) { curTooltip.close(); curTooltip.setContent(''); //hack to remove the popup, as Node-RED don't offer // and "unbind" function. May break in the future variableAddr.off('mouseenter mouseleave disabled'); curTooltip = null; } } //update name if matching old one if (previousValue && variableName.val() == previousValue) { variableName.val(curVal); } previousValue = curVal; }); //populate data variableAddr.val(variable.addr); variableName.val(variable.name); variableAddr.change(); $("#node-config-input-variables-container").append(container); } function cleanVarTable() { $("#node-config-input-variables-container").children().remove(); } function populateVarTable() { if (self.vartable) { if (typeof self.vartable == 'string') { self.vartable = JSON.parse(self.vartable); } for (var i = 0; i < self.vartable.length; i++) { generateVariable(self.vartable[i]); } } } $("#node-config-input-add-variable").click(function () { generateVariable({ name: "", addr: "" }); }); $("#node-config-melsec-endpoint-var-clean").click(cleanVarTable); populateVarTable(); // export function exportCSV() { var vars = $("#node-config-input-variables-container").children(); var lines = []; vars.each(function (i) { var elm = $(this); lines.push([ elm.find(".node-config-input-variable-addr").val(), //addr elm.find(".node-config-input-variable-name").val() //name ].join(';')); }); saveAs(new Blob([lines.join('\r\n')]), 'melsecEndpoint' + (self.name ? '_' + self.name : '') + '.csv'); } $('#node-config-melsec-endpoint-var-export').click(exportCSV); // 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; var contents = e.target.result || ''; var lines = contents.split(/[\r\n]+/); if (!lines.length) { alert('file is empty!'); return; } 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, address and name'); return; } res.push({ addr: fields[0], name: fields[1] }); } if (res.length) { cleanVarTable(); self.vartable = res; populateVarTable(); } }; reader.readAsText(file); } $('#node-config-melsec-endpoint-var-import').on('change', importCSV); $('#node-config-melsec-endpoint-var-import-btn').click(function () { $('#node-config-melsec-endpoint-var-import').click(); }) }, oneditsave: function () { var node = this; var vars = $("#node-config-input-variables-container").children(); node.vartable = []; vars.each(function (i) { var elm = $(this); var addr = elm.find(".node-config-input-variable-addr").val(); var name = elm.find(".node-config-input-variable-name").val(); var v = { addr: addr, name: name || addr } node.vartable.push(v); }); } }); </script> <!-- ######################################################################################## --> <script type="text/html" data-template-name="melsec fx in"> <div class="form-row" style="min-width: 550px"> <label for="node-input-endpoint"> <i class="fa fa-cog"></i> <span data-i18n="melsec.in.label.endpoint"></span> </label> <input type="text" id="node-input-endpoint" data-i18n="[placeholder]melsec.in.label.endpoint"> </div> <div class="form-row"> <label for="node-input-mode"><i class="fa fa-sliders"></i> <span data-i18n="melsec.in.label.mode"></span></label> <select type="text" id="node-input-mode"> <option value="single" data-i18n="melsec.in.mode.single"></option> <option value="all-split" data-i18n="melsec.in.mode.all-split"></option> <option value="all" data-i18n="melsec.in.mode.all"></option> </select> </div> <div class="form-row melsec-input-var-row"> <label for="node-input-variable"><i class="fa fa-random"></i> <span data-i18n="melsec.in.label.variable"></span></label> <select type="text" id="node-input-variable"> </select> <span id="melsec-custom-var-addr" style="margin-left:5px"></span> </div> <div class="form-row"> <label>&nbsp;</label> <input type="checkbox" id="node-input-diff" style="display: inline-block; width: auto; vertical-align: top;"> <label for="node-input-diff" style="width:70%;"><span data-i18n="melsec.in.label.diff"></span></label> </div> <div class="form-row"> <label for="node-input-name"> <i class="fa fa-tag"></i> <span data-i18n="melsec.label.name"></span> </label> <input type="text" id="node-input-name" data-i18n="[placeholder]melsec.label.name"> </div> </script> <script type="text/html" data-help-name="melsec fx in"> <p>Reads data from a Mitsubishi FX PLC</p> <p>This node was created by <a href="https://st-one.io" target="_blank">ST-One</a></p> <h3>Outputs</h3> <dl class="message-properties"> <dt>payload<span class="property-type">any</span></dt> <dd> The value(s) as read from the PLC. The format and type of the payload depends on the configured "Mode" </dd> <dt>topic<span class="property-type">string</span></dt> <dd> The name of the variable, when the message refers to a single variable (that is, when mode is "Single Variable" or "All variables, one per message") </dd> </dl> <h3>Details</h3> <p> All data is read cyclically from the PLC as configured in the <i>melsec endpoint</i>, but there are three modes of making it available in a flow: </p> <ul> <li> <b>Single variable:</b> A single variable can be selected from the configured variables, and a message is sent every cycle, or only when it changes if <i>diff</i> is checked. <code>msg.payload</code> contains the variable's value and <code>msg.topic</code> has the variable's name. </li> <li> <b>All variables, one per message:</b> Like the <i>Single variable</i> mode, but for all variables configured. If <i>diff</i> is checked, a message is sent everytime any variable changes. If <i>diff</i> is unchecked, one message is sent for every variable, in every cycle. Care must be taken about the number of messages per second in this mode. </li> <li> <b>All variables:</b> In this mode, <code>msg.payload</code> contains an object with all configured variables and their values. If <i>diff</i> is checked, a message is sent if at least one of the variables changes its value. </li> </ul> </script> <script type="text/javascript"> (function () { RED.nodes.registerType('melsec fx in', { category: 'plc', color: '#F35770', defaults: { endpoint: { value: "", type: "melsec fx endpoint" }, mode: { value: "single" }, variable: { value: "" }, diff: { value: true }, name: { value: "" } }, inputs: 0, outputs: 1, icon: "serial.png", paletteLabel: "melsec fx in", label: function () { if (this.name) return this.name; return this._("melsec.in.label.name"); }, labelStyle: function () { return this.name ? "node_label_italic" : ""; }, oneditprepare: function () { var self = this; var varList = $('#node-input-variable'); var varAddr = $('#melsec-custom-var-addr'); var modeList = $('#node-input-mode'); var endpointList = $("#node-input-endpoint"); var vars = []; function updateVarList(endpointId) { $('#node-input-variable option').remove(); var endpointNode = RED.nodes.node(endpointId); if (!endpointNode) return; vars = endpointNode.vartable || []; if (typeof vars === 'string') vars = JSON.parse(vars); varList.append($('<option/>', { disabled: "disabled", selected: "selected", style: "display:none;", text: vars.length ? self._("melsec.in.label.variable-select") : self._("melsec.in.label.variable-novar") })); $.each(vars, function (i, val) { varList.append($('<option/>', { value: val.name || val.addr, text: val.name || val.addr })); if (val.name == self.variable) { varList.val(self.variable); } }); } varList.change(function () { $.each(vars, function (i, val) { if (varList.val() == val.name) { varAddr[0].innerText = val.addr; return true; } }); }); endpointList.change(function () { updateVarList(endpointList.val()); }); updateVarList(self.endpoint); modeList.change(function () { if (modeList.val() == "single") { varList.parent().show(); } else { varList.parent().hide(); } }); modeList.change(); } }); })(); </script> <!-- ######################################################################################## --> <script type="text/html" data-template-name="melsec fx control"> <div class="form-row"> <label for="node-input-endpoint"><i class="fa fa-bolt"></i> <span data-i18n="melsec.control.label.endpoint"></span></label> <input type="text" id="node-input-endpoint" data-i18n="[placeholder]melsec.control.label.endpoint"> </div> <div class="form-row"> <label for="node-input-function"><i class="fa fa-sliders"></i> <span data-i18n="melsec.control.label.function"></span></label> <select type="text" id="node-input-function"> <option value="cycletime" data-i18n="melsec.control.function.cycletime"></option> <option value="trigger" data-i18n="melsec.control.function.trigger"></option> </select> </div> <div class="form-row"> <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="melsec.label.name"></span></label> <input type="text" id="node-input-name" data-i18n="[placeholder]melsec.label.name"> </div> </script> <script type="text/html" data-help-name="melsec fx control"> <p>Enables advanced control of the PLC and the connection</p> <p>This node was created by <a href="https://st-one.io" target="_blank">ST-One</a></p> <h3>Details</h3> <p>The behavior of this node is changed according to the selected function. Each function has its own configuration, expects different parameters in the messages, and sends different messages out </p> <dl class="message-properties"> <dt>Cycle Time</dt> <dd> Changes the time interval between each cyclic read of variables. It expects a message with <code>payload</code> with a positive number, being the time in milliseconds between each read. A value of zero disables the cyclic read. </dd> <dt>Trigger read</dt> <dd> Manually triggers a read cycle. No message parameters are used and the same message is sent on the output. Useful when longer cycle times are used, but an instant feedback is needed (for example after changing a variable). Note that the <i>melsec in</i> nodes are still required to read the values of the variables. </dd> </dl> </script> <script type="text/javascript"> RED.nodes.registerType('melsec fx control', { category: 'plc', defaults: { endpoint: { value: "", type: "melsec fx endpoint", required: true }, function: { value: "cycletime" }, name: { value: "" } }, color: "#F35770", inputs: 1, outputs: 1, icon: "serial.png", paletteLabel: "melsec fx control", label: function () { if (this.name) return this.name; return this._("melsec.control.label.name"); }, labelStyle: function () { return this.name ? "node_label_italic" : ""; } }); </script>