@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 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>
@@ -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 &gt; B)</option>
114
+ <option value="GE">GE – Greater or EQal (A &gt;= B)</option>
115
+ <option value="LT">LT – LTs Than (A &lt; B)</option>
116
+ <option value="LE">LE – LTs or EQal (A &lt;= 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 &amp; 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 &nbsp;|&nbsp; ⚫ Grey ring = FALSE &nbsp;|&nbsp; 🔴 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
+ }