@vitormnm/node-red-instructions-ladder-iec-61131-3 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/icons/ladder-climb-icon.svg +23 -0
- package/icons/ladder.svg +20 -0
- package/instructions_ladder_IEC_61131_3.html +218 -0
- package/instructions_ladder_IEC_61131_3.js +246 -0
- package/package.json +33 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vitor Mião
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# @vitormnm/node-red-instructions-ladder-IEC-61131-3
|
|
2
|
+
|
|
3
|
+
A Node-RED node that provides common **IEC 61131-3 Ladder Logic instructions** for building PLC-style logic flows directly inside Node-RED.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
### Contacts
|
|
8
|
+
|
|
9
|
+
| Instruction | Description |
|
|
10
|
+
| ----------- | --------------------------------------------------------------------------- |
|
|
11
|
+
| NO | Normally Open contact. Passes execution when the source value is `true`. |
|
|
12
|
+
| NC | Normally Closed contact. Passes execution when the source value is `false`. |
|
|
13
|
+
|
|
14
|
+
### Comparison Instructions
|
|
15
|
+
|
|
16
|
+
| Instruction | Description |
|
|
17
|
+
| ----------- | ---------------------------- |
|
|
18
|
+
| EQ | Equal (`==`) |
|
|
19
|
+
| NEQ | Not Equal (`!=`) |
|
|
20
|
+
| GT | Greater Than (`>`) |
|
|
21
|
+
| GE | Greater Than or Equal (`>=`) |
|
|
22
|
+
| LT | Less Than (`<`) |
|
|
23
|
+
| LE | Less Than or Equal (`<=`) |
|
|
24
|
+
|
|
25
|
+
### Math Instructions
|
|
26
|
+
|
|
27
|
+
| Instruction | Description |
|
|
28
|
+
| ----------- | -------------- |
|
|
29
|
+
| ADD | Addition |
|
|
30
|
+
| SUB | Subtraction |
|
|
31
|
+
| MUL | Multiplication |
|
|
32
|
+
| DIV | Division |
|
|
33
|
+
| MOD | Modulus |
|
|
34
|
+
| ABS | Absolute value |
|
|
35
|
+
| SQR | Square root |
|
|
36
|
+
| MOV | Move value |
|
|
37
|
+
|
|
38
|
+
### Output Instructions
|
|
39
|
+
|
|
40
|
+
| Instruction | Description |
|
|
41
|
+
| ----------- | ---------------------------------------- |
|
|
42
|
+
| SET | Sets the destination variable to `true` |
|
|
43
|
+
| RESET | Sets the destination variable to `false` |
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Supported Data Sources
|
|
48
|
+
|
|
49
|
+
Input values can be read from:
|
|
50
|
+
|
|
51
|
+
* `msg`
|
|
52
|
+
* `flow`
|
|
53
|
+
* `global`
|
|
54
|
+
* Numeric constants
|
|
55
|
+
* Boolean constants
|
|
56
|
+
* String constants
|
|
57
|
+
|
|
58
|
+
Output values can be written to:
|
|
59
|
+
|
|
60
|
+
* `msg`
|
|
61
|
+
* `flow`
|
|
62
|
+
* `global`
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Status Display
|
|
67
|
+
|
|
68
|
+
The node provides a compact real-time status display similar to PLC ladder diagnostics.
|
|
69
|
+
|
|
70
|
+
Examples:
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
NO motor_run(true)→TRUE
|
|
74
|
+
|
|
75
|
+
GT temp(85)>limit(80)→TRUE
|
|
76
|
+
|
|
77
|
+
ADD a(10)+b(5)→result=15
|
|
78
|
+
|
|
79
|
+
DIV x(9)/y(0)→÷0ERR
|
|
80
|
+
|
|
81
|
+
SET →motor=TRUE
|
|
82
|
+
|
|
83
|
+
RST →alarm=FALSE
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The status indicator also changes color:
|
|
87
|
+
|
|
88
|
+
* Green = instruction result is TRUE
|
|
89
|
+
* Gray = instruction result is FALSE
|
|
90
|
+
* Red = execution error
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Example
|
|
95
|
+
|
|
96
|
+
### Greater Than Comparison
|
|
97
|
+
|
|
98
|
+
```text
|
|
99
|
+
Source A: msg.temperature
|
|
100
|
+
Source B: msg.limit
|
|
101
|
+
|
|
102
|
+
Instruction: GT
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Input:
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"temperature": 85,
|
|
110
|
+
"limit": 80
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Result:
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"ladder": {
|
|
119
|
+
"func": "GT",
|
|
120
|
+
"result": true
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
### Addition
|
|
128
|
+
|
|
129
|
+
```text
|
|
130
|
+
Source A: msg.value1
|
|
131
|
+
Source B: msg.value2
|
|
132
|
+
|
|
133
|
+
Instruction: ADD
|
|
134
|
+
Destination: msg.result
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Input:
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"value1": 10,
|
|
142
|
+
"value2": 5
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Output:
|
|
147
|
+
|
|
148
|
+
```json
|
|
149
|
+
{
|
|
150
|
+
"value1": 10,
|
|
151
|
+
"value2": 5,
|
|
152
|
+
"result": 15
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## IEC 61131-3 Inspired
|
|
159
|
+
|
|
160
|
+
This node is inspired by the instruction set commonly found in PLC programming environments implementing the IEC 61131-3 standard.
|
|
161
|
+
|
|
162
|
+
It allows Node-RED users to create ladder-style logic using familiar industrial automation instructions without requiring a PLC.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Installation
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
npm install node-red-contrib-iec61131-ladder
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Restart Node-RED after installation.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
MIT
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<?xml version="1.0" standalone="no"?>
|
|
2
|
+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
|
3
|
+
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
|
4
|
+
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
|
|
6
|
+
preserveAspectRatio="xMidYMid meet">
|
|
7
|
+
|
|
8
|
+
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
|
|
9
|
+
fill="#000000" stroke="none">
|
|
10
|
+
<path d="M4200 5111 c-65 -20 -131 -74 -161 -131 -7 -14 -60 -216 -118 -450
|
|
11
|
+
-98 -394 -108 -426 -131 -437 -32 -16 -1633 -19 -1674 -3 -55 20 -54 32 29
|
|
12
|
+
371 83 338 87 375 50 453 -86 181 -349 195 -449 24 -33 -56 -1149 -4606 -1149
|
|
13
|
+
-4683 0 -108 73 -207 176 -241 116 -39 251 17 307 126 9 19 66 235 125 480 92
|
|
14
|
+
379 112 448 132 467 l24 23 825 0 c543 0 832 -3 844 -10 45 -24 44 -32 -46
|
|
15
|
+
-402 -92 -379 -96 -413 -59 -492 86 -181 349 -195 449 -24 33 56 1149 4606
|
|
16
|
+
1149 4683 0 106 -72 206 -171 239 -48 17 -110 19 -152 7z m-534 -1527 c17 -25
|
|
17
|
+
15 -35 -56 -327 -101 -418 -93 -394 -138 -407 -25 -8 -288 -10 -848 -8 -782 3
|
|
18
|
+
-813 4 -833 22 -12 11 -21 29 -21 41 0 18 136 592 156 657 15 50 -26 48 879
|
|
19
|
+
48 l845 0 16 -26z m-321 -1220 c40 -16 36 -43 -51 -401 -48 -193 -93 -356
|
|
20
|
+
-101 -365 -15 -17 -69 -18 -855 -18 -825 0 -838 0 -858 20 -11 11 -20 29 -20
|
|
21
|
+
39 0 35 162 686 176 709 l14 22 839 0 c462 0 847 -3 856 -6z"/>
|
|
22
|
+
</g>
|
|
23
|
+
</svg>
|
package/icons/ladder.svg
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<?xml version="1.0" standalone="no"?>
|
|
2
|
+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
|
3
|
+
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
|
4
|
+
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
|
|
6
|
+
preserveAspectRatio="xMidYMid meet">
|
|
7
|
+
|
|
8
|
+
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
|
|
9
|
+
fill="#000000" stroke="none">
|
|
10
|
+
<path d="M1347 5098 c-22 -13 -45 -38 -57 -63 -20 -41 -20 -60 -18 -2488 l3
|
|
11
|
+
-2446 21 -28 c11 -15 41 -38 66 -51 55 -28 104 -23 149 15 57 48 59 60 59 328
|
|
12
|
+
l0 245 990 0 990 0 0 -246 c0 -271 4 -294 62 -335 72 -52 179 -25 217 55 l21
|
|
13
|
+
43 -2 2446 -3 2446 -21 28 c-11 15 -41 38 -66 51 -55 28 -104 23 -149 -15 -57
|
|
14
|
+
-48 -59 -59 -59 -333 l0 -250 -990 0 -990 0 0 251 c0 211 -3 256 -16 285 -36
|
|
15
|
+
75 -135 105 -207 62z m2203 -1193 l0 -295 -990 0 -990 0 0 295 0 295 990 0
|
|
16
|
+
990 0 0 -295z m0 -895 l0 -300 -990 0 -990 0 0 300 0 300 990 0 990 0 0 -300z
|
|
17
|
+
m0 -900 l0 -300 -990 0 -990 0 0 300 0 300 990 0 990 0 0 -300z m0 -900 l0
|
|
18
|
+
-300 -990 0 -990 0 0 300 0 300 990 0 990 0 0 -300z"/>
|
|
19
|
+
</g>
|
|
20
|
+
</svg>
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
<!-- ═══════════════════════════════════════════════════════════════════════
|
|
2
|
+
Node Registration
|
|
3
|
+
═══════════════════════════════════════════════════════════════════════ -->
|
|
4
|
+
<script type="text/javascript">
|
|
5
|
+
|
|
6
|
+
var LADDER_META = {
|
|
7
|
+
NO: { srcA: true, srcB: false, dest: false, category: 'Contact' },
|
|
8
|
+
NC: { srcA: true, srcB: false, dest: false, category: 'Contact' },
|
|
9
|
+
EQ: { srcA: true, srcB: true, dest: false, category: 'Compare' },
|
|
10
|
+
NEQ: { srcA: true, srcB: true, dest: false, category: 'Compare' },
|
|
11
|
+
GT: { srcA: true, srcB: true, dest: false, category: 'Compare' },
|
|
12
|
+
GE: { srcA: true, srcB: true, dest: false, category: 'Compare' },
|
|
13
|
+
LT: { srcA: true, srcB: true, dest: false, category: 'Compare' },
|
|
14
|
+
LE: { srcA: true, srcB: true, dest: false, category: 'Compare' },
|
|
15
|
+
ADD: { srcA: true, srcB: true, dest: true, category: 'Math' },
|
|
16
|
+
SUB: { srcA: true, srcB: true, dest: true, category: 'Math' },
|
|
17
|
+
MUL: { srcA: true, srcB: true, dest: true, category: 'Math' },
|
|
18
|
+
DIV: { srcA: true, srcB: true, dest: true, category: 'Math' },
|
|
19
|
+
MOD: { srcA: true, srcB: true, dest: true, category: 'Math' },
|
|
20
|
+
MOV: { srcA: true, srcB: false, dest: true, category: 'Math' },
|
|
21
|
+
ABS: { srcA: true, srcB: false, dest: true, category: 'Math' },
|
|
22
|
+
SQR: { srcA: true, srcB: false, dest: true, category: 'Math' },
|
|
23
|
+
SET: { srcA: false, srcB: false, dest: true, category: 'Coil' },
|
|
24
|
+
RESET: { srcA: false, srcB: false, dest: true, category: 'Coil' }
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
RED.nodes.registerType('instructions-ladder-IEC-61131-3', {
|
|
28
|
+
category: 'function',
|
|
29
|
+
color: '#C0392B',
|
|
30
|
+
defaults: {
|
|
31
|
+
name: { value: '' },
|
|
32
|
+
ladderFunc: { value: 'NO' },
|
|
33
|
+
srcA: { value: 'payload', validate: function(v) {
|
|
34
|
+
var meta = LADDER_META[$('#node-input-ladderFunc').val()] || {};
|
|
35
|
+
return !meta.srcA || (v !== undefined && v !== '');
|
|
36
|
+
}},
|
|
37
|
+
srcAType: { value: 'msg' },
|
|
38
|
+
srcB: { value: 'payload', validate: function(v) {
|
|
39
|
+
var meta = LADDER_META[$('#node-input-ladderFunc').val()] || {};
|
|
40
|
+
return !meta.srcB || (v !== undefined && v !== '');
|
|
41
|
+
}},
|
|
42
|
+
srcBType: { value: 'msg' },
|
|
43
|
+
dest: { value: 'payload', validate: function(v) {
|
|
44
|
+
var meta = LADDER_META[$('#node-input-ladderFunc').val()] || {};
|
|
45
|
+
return !meta.dest || (v !== undefined && v !== '');
|
|
46
|
+
}},
|
|
47
|
+
destType: { value: 'msg' }
|
|
48
|
+
},
|
|
49
|
+
inputs: 1,
|
|
50
|
+
outputs: 1,
|
|
51
|
+
icon: 'ladder.svg',
|
|
52
|
+
paletteLabel: 'ladder iec',
|
|
53
|
+
|
|
54
|
+
label: function () {
|
|
55
|
+
return this.name || (this.ladderFunc);
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
labelStyle: function () {
|
|
59
|
+
return this.name ? 'node_label_italic' : '';
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
oneditprepare: function () {
|
|
63
|
+
$('#node-input-srcA').typedInput({
|
|
64
|
+
typeField: '#node-input-srcAType',
|
|
65
|
+
types: ['msg', 'flow', 'global', 'num', 'bool', 'str']
|
|
66
|
+
});
|
|
67
|
+
$('#node-input-srcB').typedInput({
|
|
68
|
+
typeField: '#node-input-srcBType',
|
|
69
|
+
types: ['msg', 'flow', 'global', 'num', 'bool', 'str']
|
|
70
|
+
});
|
|
71
|
+
$('#node-input-dest').typedInput({
|
|
72
|
+
typeField: '#node-input-destType',
|
|
73
|
+
types: ['msg', 'flow', 'global']
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
function updateFields() {
|
|
77
|
+
var fn = $('#node-input-ladderFunc').val();
|
|
78
|
+
var meta = LADDER_META[fn] || {};
|
|
79
|
+
$('#row-srcA').toggle(!!meta.srcA);
|
|
80
|
+
$('#row-srcB').toggle(!!meta.srcB);
|
|
81
|
+
$('#row-dest').toggle(!!meta.dest);
|
|
82
|
+
$('#ladder-category').text(meta.category ? '[' + meta.category + ']' : '');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
$('#node-input-ladderFunc').on('change', updateFields);
|
|
86
|
+
updateFields();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<!-- ═══════════════════════════════════════════════════════════════════════
|
|
92
|
+
Edit Dialog
|
|
93
|
+
═══════════════════════════════════════════════════════════════════════ -->
|
|
94
|
+
<script type="text/html" data-template-name="instructions-ladder-IEC-61131-3">
|
|
95
|
+
|
|
96
|
+
<div class="form-row">
|
|
97
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
98
|
+
<input type="text" id="node-input-name" placeholder="Name (optional)">
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<hr/>
|
|
102
|
+
|
|
103
|
+
<div class="form-row">
|
|
104
|
+
<label for="node-input-ladderFunc"><i class="fa fa-microchip"></i> Instruction</label>
|
|
105
|
+
<select id="node-input-ladderFunc" style="width:70%;">
|
|
106
|
+
<optgroup label="── Contacts ──">
|
|
107
|
+
<option value="NO">-||- Normally Open Contact </option>
|
|
108
|
+
<option value="NC">-|/|- Normally Closed Contact</option>
|
|
109
|
+
</optgroup>
|
|
110
|
+
<optgroup label="── Comparators ──">
|
|
111
|
+
<option value="EQ">EQ – EQal (A == B)</option>
|
|
112
|
+
<option value="NEQ">NEQ – Not EQal (A != B)</option>
|
|
113
|
+
<option value="GT">GT – Greater Than (A > B)</option>
|
|
114
|
+
<option value="GE">GE – Greater or EQal (A >= B)</option>
|
|
115
|
+
<option value="LT">LT – LTs Than (A < B)</option>
|
|
116
|
+
<option value="LE">LE – LTs or EQal (A <= B)</option>
|
|
117
|
+
</optgroup>
|
|
118
|
+
<optgroup label="── Math ──">
|
|
119
|
+
<option value="ADD">ADD – Add (A + B → dest)</option>
|
|
120
|
+
<option value="SUB">SUB – Subtract (A - B → dest)</option>
|
|
121
|
+
<option value="MUL">MUL – Multiply (A * B → dest)</option>
|
|
122
|
+
<option value="DIV">DIV – Divide (A / B → dest)</option>
|
|
123
|
+
<option value="MOD">MOD – Modulo (A % B → dest)</option>
|
|
124
|
+
<option value="MOV">MOV – Move (A → dest)</option>
|
|
125
|
+
<option value="ABS">ABS – Absolute Value (|A| → dest)</option>
|
|
126
|
+
<option value="SQR">SQR – Square Root (√A → dest)</option>
|
|
127
|
+
</optgroup>
|
|
128
|
+
<optgroup label="── Output Coils ──">
|
|
129
|
+
<option value="SET">-(S)- Set/Latch</option>
|
|
130
|
+
<option value="RESET">-(R)- Reset/Unlatch</option>
|
|
131
|
+
</optgroup>
|
|
132
|
+
</select>
|
|
133
|
+
<span id="ladder-category" style="margin-left:8px; color:#999; font-size:0.85em;"></span>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<hr/>
|
|
137
|
+
|
|
138
|
+
<div class="form-row" id="row-srcA">
|
|
139
|
+
<label for="node-input-srcA"><i class="fa fa-arrow-right"></i> Source A</label>
|
|
140
|
+
<input type="hidden" id="node-input-srcAType">
|
|
141
|
+
<input type="text" id="node-input-srcA" style="width:60%;">
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<div class="form-row" id="row-srcB">
|
|
145
|
+
<label for="node-input-srcB"><i class="fa fa-arrow-right"></i> Source B</label>
|
|
146
|
+
<input type="hidden" id="node-input-srcBType">
|
|
147
|
+
<input type="text" id="node-input-srcB" style="width:60%;">
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div class="form-row" id="row-dest">
|
|
151
|
+
<label for="node-input-dest"><i class="fa fa-arrow-left"></i> Destination</label>
|
|
152
|
+
<input type="hidden" id="node-input-destType">
|
|
153
|
+
<input type="text" id="node-input-dest" style="width:60%;">
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<hr/>
|
|
157
|
+
|
|
158
|
+
<div class="form-row" style="color:#888; font-size:0.85em;">
|
|
159
|
+
<i class="fa fa-info-circle"></i>
|
|
160
|
+
Result is sent in <code>msg.payload</code> (true/false) and <code>msg.ladder</code>.<br>
|
|
161
|
+
<i class="fa fa-exclamation-triangle" style="color:#e67e22;"></i>
|
|
162
|
+
Contacts & comparators only forward the message when the condition is <strong>TRUE</strong>.
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
</script>
|
|
166
|
+
|
|
167
|
+
<!-- ═══════════════════════════════════════════════════════════════════════
|
|
168
|
+
Help Panel
|
|
169
|
+
═══════════════════════════════════════════════════════════════════════ -->
|
|
170
|
+
<script type="text/html" data-help-name="instructions-ladder-IEC-61131-3">
|
|
171
|
+
<p>
|
|
172
|
+
Simulates <strong>Ladder Logic</strong> instructions from
|
|
173
|
+
<em>Studio 5000 Logix Designer</em> (Rockwell Automation) inside Node-RED.
|
|
174
|
+
</p>
|
|
175
|
+
|
|
176
|
+
<h3>Contacts</h3>
|
|
177
|
+
<dl>
|
|
178
|
+
<dt>NO – Examine If Closed</dt><dd>TRUE when Source A is non-zero/truthy. Message forwarded only when TRUE.</dd>
|
|
179
|
+
<dt>NC – Examine If Open</dt><dd>TRUE when Source A is zero/falsy. Message forwarded only when TRUE.</dd>
|
|
180
|
+
</dl>
|
|
181
|
+
|
|
182
|
+
<h3>Comparators</h3>
|
|
183
|
+
<dl>
|
|
184
|
+
<dt>EQ / NEQ</dt><dd>EQal / Not EQal. Message forwarded only when TRUE.</dd>
|
|
185
|
+
<dt>GT / GE</dt><dd>Greater Than / Greater or EQal.</dd>
|
|
186
|
+
<dt>LT / LE</dt><dd>LTs Than / LTs or EQal.</dd>
|
|
187
|
+
</dl>
|
|
188
|
+
|
|
189
|
+
<h3>Math</h3>
|
|
190
|
+
<dl>
|
|
191
|
+
<dt>ADD / SUB / MUL / DIV / MOD</dt><dd>Arithmetic, result written to Destination. Always forwards message.</dd>
|
|
192
|
+
<dt>MOV</dt><dd>Copies Source A to Destination.</dd>
|
|
193
|
+
<dt>ABS</dt><dd>Absolute value of Source A → Destination.</dd>
|
|
194
|
+
<dt>SQR</dt><dd>Square root of Source A → Destination.</dd>
|
|
195
|
+
</dl>
|
|
196
|
+
|
|
197
|
+
<h3>Output Coils</h3>
|
|
198
|
+
<dl>
|
|
199
|
+
<dt>SET</dt><dd>Writes <code>true</code> to Destination.</dd>
|
|
200
|
+
<dt>RESET</dt><dd>Writes <code>false</code> to Destination.</dd>
|
|
201
|
+
</dl>
|
|
202
|
+
|
|
203
|
+
<h3>Outputs</h3>
|
|
204
|
+
<ul>
|
|
205
|
+
<li><code>msg.payload</code> – boolean result.</li>
|
|
206
|
+
<li><code>msg.ladder.func</code> – instruction name.</li>
|
|
207
|
+
<li><code>msg.ladder.result</code> – same as payload.</li>
|
|
208
|
+
<li><code>msg.ladder.status</code> – status string shown on node.</li>
|
|
209
|
+
</ul>
|
|
210
|
+
|
|
211
|
+
<h3>Node Status</h3>
|
|
212
|
+
<p>
|
|
213
|
+
Status is displayed in two logical parts separated by <strong>‖</strong>:<br>
|
|
214
|
+
<em>Left</em>: instruction + variable names.<br>
|
|
215
|
+
<em>Right</em>: resolved values + result.<br><br>
|
|
216
|
+
🟢 Green dot = TRUE | ⚫ Grey ring = FALSE | 🔴 Red = Error
|
|
217
|
+
</p>
|
|
218
|
+
</script>
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
module.exports = function (RED) {
|
|
2
|
+
|
|
3
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
function resolveValue(node, msg, type, key) {
|
|
6
|
+
if (key === undefined || key === '') return undefined;
|
|
7
|
+
switch (type) {
|
|
8
|
+
case 'msg': return RED.util.getMessageProperty(msg, key);
|
|
9
|
+
case 'flow': return node.context().flow.get(key);
|
|
10
|
+
case 'global': return node.context().global.get(key);
|
|
11
|
+
case 'num': return Number(key);
|
|
12
|
+
case 'bool': return key === 'true' || key === true;
|
|
13
|
+
case 'str': return String(key);
|
|
14
|
+
default: return key;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
// Formats a value for status: rounds floats to 4 decimal places
|
|
20
|
+
function fmt(v) {
|
|
21
|
+
if (typeof v === 'number' && !Number.isInteger(v)) return +v.toFixed(4);
|
|
22
|
+
return v;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ─── Status builder ───────────────────────────────────────────────────────
|
|
26
|
+
//
|
|
27
|
+
// Node-RED status text is a single string — no real newline.
|
|
28
|
+
// We simulate two logical lines with a ‖ separator:
|
|
29
|
+
// Line 1: instruction mnemonic + operand variable names
|
|
30
|
+
// Line 2: resolved values + result/output
|
|
31
|
+
//
|
|
32
|
+
// ExampLT:
|
|
33
|
+
// NO (payload) ‖ val=true → TRUE
|
|
34
|
+
// GT (temp) > (limit) ‖ 85 > 80 → TRUE
|
|
35
|
+
// ADD (a) + (b) → (result) ‖ 10 + 5 = 15
|
|
36
|
+
// SET → (motor) ‖ (motor) = true
|
|
37
|
+
|
|
38
|
+
// ─── Status builder ───────────────────────────────────────────────────────
|
|
39
|
+
//
|
|
40
|
+
// Format: FN varA(valA)op varB(valB)→dest=result (no spaces, max compact)
|
|
41
|
+
//
|
|
42
|
+
// Examples:
|
|
43
|
+
// NO motor_run(true)→TRUE
|
|
44
|
+
// GT temp(85)>limit(80)→TRUE
|
|
45
|
+
// ADD a(10)+b(5)→result=15
|
|
46
|
+
// DIV x(9)/y(0)→÷0ERR
|
|
47
|
+
// MOV src(42)→dest
|
|
48
|
+
// ABS |x(-7)|→out=7
|
|
49
|
+
// SET →motor=TRUE
|
|
50
|
+
// RST →alarm=FALSE
|
|
51
|
+
|
|
52
|
+
function buildStatus(fn, nameA, nameB, nameDest, valA, valB, result, computed) {
|
|
53
|
+
const nA = nameA || '?';
|
|
54
|
+
const nB = nameB || '?';
|
|
55
|
+
const nD = nameDest || '?';
|
|
56
|
+
const vA = fmt(valA);
|
|
57
|
+
const vB = fmt(valB);
|
|
58
|
+
const res = result ? 'TRUE' : 'FALSE';
|
|
59
|
+
|
|
60
|
+
// ── Contacts ─────────────────────────────────────────────────────────
|
|
61
|
+
// NO motor_run(true)→TRUE
|
|
62
|
+
if (fn === 'NO' || fn === 'NC')
|
|
63
|
+
return `${fn} ${nA}(${vA})→${res}`;
|
|
64
|
+
|
|
65
|
+
// ── Comparators ──────────────────────────────────────────────────────
|
|
66
|
+
// GT temp(85)>limit(80)→TRUE
|
|
67
|
+
const OPS = { EQ: '==', NEQ: '!=', GT: '>', GE: '>=', LT: '<', LE: '<=' };
|
|
68
|
+
if (OPS[fn])
|
|
69
|
+
return `${fn} ${nA}(${vA})${OPS[fn]}${nB}(${vB})→${res}`;
|
|
70
|
+
|
|
71
|
+
// ── Math (two operands) ───────────────────────────────────────────────
|
|
72
|
+
// ADD a(10)+b(5)→result=15
|
|
73
|
+
// DIV x(9)/y(0)→÷0ERR
|
|
74
|
+
const MATH2 = { ADD: '+', SUB: '-', MUL: '*', DIV: '/', MOD: '%' };
|
|
75
|
+
if (MATH2[fn]) {
|
|
76
|
+
if ((fn === 'DIV' || fn === 'MOD') && Number(valB) === 0)
|
|
77
|
+
return `${fn} ${nA}(${vA})${MATH2[fn]}${nB}(0)→÷0ERR`;
|
|
78
|
+
return `${fn} ${nA}(${vA})${MATH2[fn]}${nB}(${vB})→${nD}=${fmt(computed)}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Math (one operand) ────────────────────────────────────────────────
|
|
82
|
+
// MOV src(42)→dest
|
|
83
|
+
// ABS |x(-7)|→out=7
|
|
84
|
+
// SQR √n(64)→root=8
|
|
85
|
+
if (fn === 'MOV') return `MOV ${nA}(${vA})→${nD}`;
|
|
86
|
+
if (fn === 'ABS') return `ABS |${nA}(${vA})|→${nD}=${fmt(computed)}`;
|
|
87
|
+
if (fn === 'SQR') return `SQR √${nA}(${vA})→${nD}=${fmt(computed)}`;
|
|
88
|
+
|
|
89
|
+
// ── Output Coils ──────────────────────────────────────────────────────
|
|
90
|
+
// SET →motor=TRUE | RST →alarm=FALSE
|
|
91
|
+
if (fn === 'SET') return `SET →${nD}=TRUE`;
|
|
92
|
+
if (fn === 'RESET') return `RST →${nD}=FALSE`;
|
|
93
|
+
|
|
94
|
+
return `${fn} executed`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─── Instruction Execution ────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
function executeLadder(node, msg, config) {
|
|
100
|
+
|
|
101
|
+
const fn = config.ladderFunc;
|
|
102
|
+
const valA = resolveValue(node, msg, config.srcAType, config.srcA);
|
|
103
|
+
const valB = resolveValue(node, msg, config.srcBType, config.srcB);
|
|
104
|
+
const nameA = config.srcA || '';
|
|
105
|
+
const nameB = config.srcB || '';
|
|
106
|
+
const nameDest = config.dest || '';
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
let payload = null;
|
|
110
|
+
let result = false;
|
|
111
|
+
let sendMsg = true;
|
|
112
|
+
let Msg = true;
|
|
113
|
+
let computed; // for math instructions that produce a new value
|
|
114
|
+
var resultOperation = false
|
|
115
|
+
|
|
116
|
+
switch (fn) {
|
|
117
|
+
|
|
118
|
+
// ── Contacts ─────────────────────────────────────────────────────
|
|
119
|
+
case 'NO': result = !!valA; sendMsg = result; break;
|
|
120
|
+
case 'NC': result = !valA; sendMsg = result; break;
|
|
121
|
+
|
|
122
|
+
// ── Comparators ──────────────────────────────────────────────────
|
|
123
|
+
case 'EQ': result = Number(valA) === Number(valB); sendMsg = result; break;
|
|
124
|
+
case 'NEQ': result = Number(valA) !== Number(valB); sendMsg = result; break;
|
|
125
|
+
case 'GT': result = Number(valA) > Number(valB); sendMsg = result; break;
|
|
126
|
+
case 'GE': result = Number(valA) >= Number(valB); sendMsg = result; break;
|
|
127
|
+
case 'LT': result = Number(valA) < Number(valB); sendMsg = result; break;
|
|
128
|
+
case 'LE': result = Number(valA) <= Number(valB); sendMsg = result; break;
|
|
129
|
+
|
|
130
|
+
// ── Math (two operands) ───────────────────────────────────────────
|
|
131
|
+
case 'ADD':
|
|
132
|
+
computed = Number(valA) + Number(valB);
|
|
133
|
+
result = true;
|
|
134
|
+
resultOperation = true
|
|
135
|
+
break;
|
|
136
|
+
case 'SUB':
|
|
137
|
+
computed = Number(valA) - Number(valB);
|
|
138
|
+
|
|
139
|
+
result = true;
|
|
140
|
+
resultOperation = true
|
|
141
|
+
break;
|
|
142
|
+
case 'MUL':
|
|
143
|
+
computed = Number(valA) * Number(valB);
|
|
144
|
+
result = computed;
|
|
145
|
+
resultOperation = true
|
|
146
|
+
break;
|
|
147
|
+
case 'MOD':
|
|
148
|
+
if (Number(valB) === 0) { node.warn('MOD: Division by zero!'); result = false; break; }
|
|
149
|
+
computed = Number(valA) % Number(valB);
|
|
150
|
+
|
|
151
|
+
result = true;
|
|
152
|
+
resultOperation = true
|
|
153
|
+
break;
|
|
154
|
+
case 'DIV':
|
|
155
|
+
if (Number(valB) === 0) { node.warn('DIV: Division by zero!'); result = false; break; }
|
|
156
|
+
computed = Number(valA) / Number(valB);
|
|
157
|
+
|
|
158
|
+
result = true;
|
|
159
|
+
resultOperation = true
|
|
160
|
+
type = "Math"
|
|
161
|
+
break;
|
|
162
|
+
|
|
163
|
+
// ── Math (one operand) ────────────────────────────────────────────
|
|
164
|
+
case 'MOV':
|
|
165
|
+
|
|
166
|
+
result = true;
|
|
167
|
+
break;
|
|
168
|
+
case 'ABS': computed = Math.abs(Number(valA)); result = true; break;
|
|
169
|
+
case 'SQR': computed = Math.sqrt(Math.abs(Number(valA))); result = true; break;
|
|
170
|
+
|
|
171
|
+
// ── Output Coils ──────────────────────────────────────────────────
|
|
172
|
+
case 'SET':
|
|
173
|
+
resultOperation = true
|
|
174
|
+
result = true;
|
|
175
|
+
break;
|
|
176
|
+
case 'RESET':
|
|
177
|
+
resultOperation = true
|
|
178
|
+
result = false;
|
|
179
|
+
break;
|
|
180
|
+
|
|
181
|
+
default:
|
|
182
|
+
node.warn('Unknown instruction: ' + fn);
|
|
183
|
+
result = false;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const statusText = buildStatus(fn, nameA, nameB, nameDest, valA, valB, result, computed);
|
|
187
|
+
return { result, resultOperation, statusText, sendMsg };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function writeNode(node, msg, config, sendMsg, result, resultOperation) {
|
|
191
|
+
console.log(config)
|
|
192
|
+
const ladderFunc = config.ladderFunc;
|
|
193
|
+
const dest = config.dest;
|
|
194
|
+
const destType = config.destType;
|
|
195
|
+
|
|
196
|
+
switch (destType) {
|
|
197
|
+
case 'msg':
|
|
198
|
+
if (resultOperation) {
|
|
199
|
+
msg[dest] = result
|
|
200
|
+
}
|
|
201
|
+
break;
|
|
202
|
+
case 'flow': node.context().flow.set(dest, result); break;
|
|
203
|
+
case 'global': node.context().global.set(dest, result); break;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
if (sendMsg) {
|
|
208
|
+
node.send(msg);
|
|
209
|
+
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ─── Node Definition ──────────────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
function instructions_ladder_IEC_61131_3(config) {
|
|
216
|
+
RED.nodes.createNode(this, config);
|
|
217
|
+
const node = this;
|
|
218
|
+
|
|
219
|
+
node.status({ fill: 'grey', shape: 'ring', text: 'waiting...' });
|
|
220
|
+
|
|
221
|
+
node.on('input', function (msg) {
|
|
222
|
+
try {
|
|
223
|
+
const { result, resultOperation, statusText, sendMsg } = executeLadder(node, msg, config);
|
|
224
|
+
|
|
225
|
+
node.status({
|
|
226
|
+
fill: result ? 'green' : 'grey',
|
|
227
|
+
shape: result ? 'dot' : 'ring',
|
|
228
|
+
text: statusText
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
msg.ladder = { func: config.ladderFunc, result, status: statusText };
|
|
232
|
+
//Write payload and msg
|
|
233
|
+
writeNode(node, msg, config, sendMsg, result, resultOperation)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
} catch (err) {
|
|
237
|
+
node.error('Ladder execution error: ' + err.message, msg);
|
|
238
|
+
node.status({ fill: 'red', shape: 'dot', text: 'Error: ' + err.message });
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
node.on('close', function () { node.status({}); });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
RED.nodes.registerType('instructions-ladder-IEC-61131-3', instructions_ladder_IEC_61131_3);
|
|
246
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vitormnm/node-red-instructions-ladder-iec-61131-3",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"description": "Node-RED instructions ladder IEC-61131-3",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"node-red",
|
|
10
|
+
"ladder",
|
|
11
|
+
"IEC-61131-3",
|
|
12
|
+
"plc",
|
|
13
|
+
"codesys"
|
|
14
|
+
],
|
|
15
|
+
"node-red": {
|
|
16
|
+
"nodes": {
|
|
17
|
+
"version": ">=2.0.0",
|
|
18
|
+
"instructions-ladder-IEC-61131-3": "instructions_ladder_IEC_61131_3.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"author": {
|
|
22
|
+
"name": "Vitor Mião",
|
|
23
|
+
"url": "https://github.com/vitormnm"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/vitormnm/node-red-instructions-ladder-IEC-61131-3"
|
|
32
|
+
}
|
|
33
|
+
}
|