node-red-contrib-melsec
Version:
Node-RED Node to communicate with Mitsubishi FX over Programming Port
640 lines (553 loc) • 21.4 kB
HTML
<!--
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> </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>