node-red-contrib-omron-eip 0.2.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.
@@ -0,0 +1,416 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('omron-write', {
3
+ category: 'Omron',
4
+ color: '#f0a868',
5
+ defaults: {
6
+ name: { value: '' },
7
+ plc: { value: '', type: 'omron-plc', required: true },
8
+ valueSource: { value: 'msg' },
9
+ variables: { value: [] },
10
+ verified: { value: false },
11
+ pollMs: { value: 0, validate: function (v) { return v === '' || v === undefined || !isNaN(Number(v)); } },
12
+ fireOnDeploy:{ value: false },
13
+ },
14
+ inputs: 1,
15
+ outputs: 2,
16
+ outputLabels: ['result', 'error'],
17
+ icon: 'font-awesome/fa-upload',
18
+ paletteLabel: 'omron write',
19
+ label: function () {
20
+ if (this.name) return this.name;
21
+ const names = (this.variables || []).map(v => v.name).filter(Boolean);
22
+ if (names.length === 0) return 'omron write';
23
+ if (names.length <= 2) return 'write: ' + names.join(', ');
24
+ return 'write: ' + names.length + ' tags';
25
+ },
26
+ oneditprepare: function () {
27
+ const node = this;
28
+
29
+ function buildRow(row, data) {
30
+ data = data || {};
31
+ $(row).css({ display: 'flex', 'align-items': 'center' });
32
+ const name = $('<input/>', { type: 'text', class: 'omron-var-name',
33
+ placeholder: 'variable name' }).css({ flex: 1, 'margin-right': '6px' }).appendTo(row);
34
+ name.val(data.name || '');
35
+ if (node._omronTags && node._omronTags.length) {
36
+ name[0].setAttribute('list', 'omron-tags-datalist');
37
+ }
38
+ $('<span/>', { class: 'omron-var-badge' }).css({ width: '18px', 'text-align': 'center' }).appendTo(row);
39
+ if ($('#node-input-valueSource').val() === 'fixed') {
40
+ const valWrap = $('<div/>').css({ flex: 1, 'margin-left': '6px' }).appendTo(row);
41
+ const valInput = $('<input/>', { type: 'text', class: 'omron-var-value' }).appendTo(valWrap);
42
+ valInput.typedInput({ default: data.valueType || 'num', types: ['num', 'bool', 'str', 'json'] });
43
+ valInput.typedInput('value', data.value !== undefined ? data.value : '');
44
+ valInput.typedInput('type', data.valueType || 'num');
45
+ }
46
+ }
47
+
48
+ function refreshList() {
49
+ const existing = [];
50
+ $('#node-input-variable-container').find('.omron-var-name').each(function () {
51
+ const r = { name: $(this).val() };
52
+ const vi = $(this).closest('li').find('.omron-var-value');
53
+ if (vi.length) { r.value = vi.typedInput('value'); r.valueType = vi.typedInput('type'); }
54
+ existing.push(r);
55
+ });
56
+ $('#node-input-variable-container').editableList('empty');
57
+ const seed = existing.length ? existing : (node.variables || []);
58
+ seed.forEach(v => $('#node-input-variable-container').editableList('addItem', v));
59
+ }
60
+
61
+ $('#node-input-variable-container').css('min-height', '120px').editableList({
62
+ addItem: function (row, index, data) { buildRow(row, data); },
63
+ removable: true, sortable: true,
64
+ });
65
+ (this.variables || []).forEach(v => $('#node-input-variable-container').editableList('addItem', v));
66
+
67
+ $('#node-input-valueSource').on('change', function () {
68
+ const isMsg = $(this).val() === 'msg';
69
+ $('#omron-msg-hint').toggle(isMsg);
70
+ $('#omron-injson-row').toggle(isMsg);
71
+ $('#omron-col-header').toggle(!isMsg);
72
+ refreshList();
73
+ }).trigger('change');
74
+
75
+ // ---- Data type reference table (toggle) ----
76
+ const TYPEREF_ROWS = [
77
+ ['number', 'SINT', 'integer', '-128 … 127'],
78
+ ['number', 'INT', 'integer', '-32,768 … 32,767'],
79
+ ['number', 'DINT', 'integer', '-2,147,483,648 … 2,147,483,647'],
80
+ ['number', 'LINT', 'integer (64-bit)', '±9.22e18 — see note on 64-bit'],
81
+ ['number', 'USINT', 'integer', '0 … 255'],
82
+ ['number', 'UINT', 'integer', '0 … 65,535'],
83
+ ['number', 'UDINT', 'integer', '0 … 4,294,967,295'],
84
+ ['number', 'ULINT', 'integer (64-bit)', '0 … 1.84e19 — see note on 64-bit'],
85
+ ['number', 'BYTE', 'integer (8-bit bit string)', '0 … 255'],
86
+ ['number', 'WORD', 'integer (16-bit bit string)', '0 … 65,535'],
87
+ ['number', 'DWORD', 'integer (32-bit bit string)', '0 … 4,294,967,295'],
88
+ ['number', 'LWORD', 'integer (64-bit bit string)', '0 … 1.84e19 — see note on 64-bit'],
89
+ ['number', 'REAL', 'float (single)', '~7 significant digits'],
90
+ ['number', 'LREAL', 'float (double)', '~15 significant digits'],
91
+ ['number', 'ENUM', 'integer (enum value)', '0 … 4,294,967,295'],
92
+ ['boolean', 'BOOL', 'true / false', ''],
93
+ ['string', 'STRING','text', 'up to 1986 characters (this controller)'],
94
+ ['json', 'ARRAY of any type', 'a JSON array, e.g. [1,2,3]', 'whole-array write'],
95
+ ['json', 'STRUCT (UDT)', 'a JSON object, e.g. {"Speed":100}', 'whole-structure write'],
96
+ ['json', 'UNION', 'a JSON object/array', 'layout is type-specific'],
97
+ ['number', 'DATE', 'nanoseconds since 1970 (a number)', 'reads back as a date; ms precision OK'],
98
+ ['number', 'DATE_AND_TIME', 'nanoseconds since 1970 (a number)', 'reads back as a date; ms precision OK'],
99
+ ['number', 'TIME', 'nanoseconds (a number)', 'duration; ms precision OK, see note'],
100
+ ['number', 'TIME_OF_DAY', 'nanoseconds (a number)', 'ms precision OK, see note'],
101
+ ];
102
+
103
+ function buildTypeRef() {
104
+ let html = '<table class="omron-typeref-table">';
105
+ html += '<tr><th>Node-RED type</th><th>Sysmac / Omron type</th><th>How to enter the value</th><th>Range / notes</th></tr>';
106
+ TYPEREF_ROWS.forEach(r => {
107
+ html += '<tr><td><code>' + r[0] + '</code></td><td><b>' + r[1] + '</b></td><td>' + r[2] + '</td><td>' + r[3] + '</td></tr>';
108
+ });
109
+ html += '</table>';
110
+ html += '<div class="omron-typeref-note">' +
111
+ '<b>How it works:</b> the value-type dropdown (number / boolean / string / JSON) is a ' +
112
+ '<i>Node-RED</i> type — it is not the PLC type. The library knows each tag\u2019s real ' +
113
+ 'Omron type and encodes your value automatically. For every numeric Omron type, choose ' +
114
+ '<code>number</code>; for BOOL choose <code>boolean</code>; for STRING choose ' +
115
+ '<code>string</code>; for arrays and structures choose <code>JSON</code>.<br><br>' +
116
+ '<b>64-bit note (LINT / ULINT / LWORD):</b> these exceed JavaScript\u2019s safe integer ' +
117
+ 'limit (about 9.0e15). Values within the safe range work as plain numbers; for values ' +
118
+ 'beyond it, precision can be lost when entering them as a JS number — handle very large ' +
119
+ '64-bit values with care.<br><br>' +
120
+ '<b>Date &amp; time types (DATE, DATE_AND_TIME, TIME, TIME_OF_DAY):</b> send a ' +
121
+ '<code>number</code> of <i>nanoseconds</i>. DATE / DATE_AND_TIME use nanoseconds since ' +
122
+ '1970-01-01 UTC (epoch&nbsp;ms &times; 1,000,000) and read back as a date; TIME / ' +
123
+ 'TIME_OF_DAY are a nanosecond duration. Verified to <b>millisecond</b> precision. ' +
124
+ 'Because the nanosecond value is a very large (19-digit) integer, sub-millisecond ' +
125
+ 'digits can be lost when sent as a plain number, so values are reliable down to the ' +
126
+ 'millisecond. String input is not accepted for these types — send a number.<br><br>' +
127
+ '<b>Out-of-range values are rejected</b> with a clear error (e.g. a BYTE only accepts ' +
128
+ '0\u2013255).</div>';
129
+ return html;
130
+ }
131
+
132
+ $('#omron-typeref-btn').on('click', function () {
133
+ const box = $('#omron-typeref');
134
+ if (box.is(':empty')) box.html(buildTypeRef());
135
+ box.toggle();
136
+ });
137
+
138
+ // ---- Show input JSON (message mode) ----
139
+ function exampleValue(name) {
140
+ const types = node._omronTypes || {};
141
+ const base = name.split(/[.\[]/)[0];
142
+ const cat = types[name] || types[base];
143
+ const isIndexedOrMember = /[.\[]/.test(name);
144
+ switch (cat) {
145
+ case 'bool': return false;
146
+ case 'float': return 0.0;
147
+ case 'number': return 0;
148
+ case 'string': return '';
149
+ case 'datetime': return '<date/time>';
150
+ case 'array': return isIndexedOrMember ? 0 : [];
151
+ case 'struct': return isIndexedOrMember ? 0 : {};
152
+ default: return '<value>';
153
+ }
154
+ }
155
+
156
+ function buildInputJson() {
157
+ const names = [];
158
+ $('#node-input-variable-container').find('.omron-var-name').each(function () {
159
+ const n = $(this).val().trim();
160
+ if (n) names.push(n);
161
+ });
162
+ const validated = node._omronTypes && Object.keys(node._omronTypes).length > 0;
163
+ if (names.length === 0) {
164
+ return '<div class="omron-typeref-note">Add one or more variables first.</div>';
165
+ }
166
+ let html = '';
167
+ let jsonText;
168
+ if (names.length === 1) {
169
+ // Single variable: bare value payload.
170
+ const v = exampleValue(names[0]);
171
+ jsonText = JSON.stringify(v, null, 2);
172
+ html += '<div class="omron-typeref-note">One variable — set <code>msg.payload</code> ' +
173
+ 'to the value directly:</div>';
174
+ html += '<pre class="omron-json" style="background:#f4f4f4;color:#1a1a1a;border:1px solid #d0d0d0;border-radius:3px;padding:8px 10px;font-family:monospace;font-size:12px;line-height:1.45;white-space:pre;overflow-x:auto;margin:6px 0;">msg.payload = ' + jsonText + '</pre>';
175
+ } else {
176
+ // Multiple variables: object keyed by name.
177
+ const obj = {};
178
+ names.forEach(n => { obj[n] = exampleValue(n); });
179
+ jsonText = JSON.stringify(obj, null, 2);
180
+ html += '<div class="omron-typeref-note">Multiple variables — set ' +
181
+ '<code>msg.payload</code> to an object keyed by variable name:</div>';
182
+ html += '<pre class="omron-json" style="background:#f4f4f4;color:#1a1a1a;border:1px solid #d0d0d0;border-radius:3px;padding:8px 10px;font-family:monospace;font-size:12px;line-height:1.45;white-space:pre;overflow-x:auto;margin:6px 0;">' + jsonText + '</pre>';
183
+ }
184
+ if (!validated) {
185
+ html += '<div class="omron-typeref-note"><i class="fa fa-info-circle"></i> ' +
186
+ 'Values shown are generic placeholders. Press <b>Test Connection, Load and ' +
187
+ 'Validate Published Variables</b> to fill in the actual types ' +
188
+ '(numbers, booleans, strings, arrays, structures).</div>';
189
+ }
190
+ html += '<button type="button" class="red-ui-button red-ui-button-small omron-copy-btn">' +
191
+ '<i class="fa fa-copy"></i> Copy</button>';
192
+ return html;
193
+ }
194
+
195
+ $('#omron-injson-btn').on('click', function () {
196
+ const box = $('#omron-injson');
197
+ box.html(buildInputJson());
198
+ box.toggle();
199
+ box.find('.omron-copy-btn').on('click', function () {
200
+ let txt = box.find('pre.omron-json').first().text();
201
+ txt = txt.replace(/^msg\.payload = /, ''); // copy just the value/object
202
+ navigator.clipboard && navigator.clipboard.writeText(txt);
203
+ $(this).html('<i class="fa fa-check"></i> Copied');
204
+ setTimeout(() => $(this).html('<i class="fa fa-copy"></i> Copy'), 1500);
205
+ });
206
+ });
207
+
208
+ $('#omron-validate-btn').on('click', function () {
209
+ const cfgId = $('#node-input-plc').val();
210
+ const statusEl = $('#omron-validate-status');
211
+ if (!cfgId || cfgId === '_ADD_') {
212
+ statusEl.html('<i class="fa fa-exclamation-triangle"></i> Select & deploy a PLC config first.').css('color', '#b30000');
213
+ return;
214
+ }
215
+ statusEl.html('<i class="fa fa-spinner fa-spin"></i> Connecting…').css('color', '#888');
216
+ $.getJSON('omron-eip/' + cfgId + '/tags')
217
+ .done(function (data) {
218
+ const tags = data.tags || [];
219
+ statusEl.html('<i class="fa fa-check"></i> ' + tags.length + ' tags loaded').css('color', '#3a7d3a');
220
+ $('#node-input-variable-container').find('.omron-var-name').each(function () {
221
+ const base = $(this).val().split(/[.\[]/)[0];
222
+ const badge = $(this).siblings('.omron-var-badge');
223
+ if (!base) { badge.text(''); return; }
224
+ if (tags.indexOf(base) !== -1) badge.html('<i class="fa fa-check" style="color:#3a7d3a;" title="Green check: this tag name was found on the controller — it exists and can be written."></i>');
225
+ else badge.html('<i class="fa fa-times" style="color:#b30000;" title="Red X: this tag name was NOT found on the controller. Usually a spelling mistake, or the tag has not been shared to the network (in Sysmac Studio its Network Publish must be set to Publish Only)."></i>');
226
+ });
227
+ let dl = $('#omron-tags-datalist');
228
+ if (dl.length === 0) dl = $('<datalist id="omron-tags-datalist"></datalist>').appendTo('body');
229
+ dl.empty();
230
+ tags.forEach(t => dl.append($('<option>').attr('value', t)));
231
+ node._omronTags = tags;
232
+ node._omronTypes = data.types || {};
233
+ $('#node-input-variable-container').find('.omron-var-name').each(function () {
234
+ this.setAttribute('list', 'omron-tags-datalist');
235
+ this.dispatchEvent(new Event('input', { bubbles: true }));
236
+ });
237
+ })
238
+ .fail(function (xhr) {
239
+ const e = (xhr.responseJSON && xhr.responseJSON.error) || 'connection failed';
240
+ statusEl.html('<i class="fa fa-times"></i> ' + e).css('color', '#b30000');
241
+ });
242
+ });
243
+ },
244
+ oneditsave: function () {
245
+ const isFixed = $('#node-input-valueSource').val() === 'fixed';
246
+ const vars = [];
247
+ $('#node-input-variable-container').find('.omron-var-name').each(function () {
248
+ const name = $(this).val().trim();
249
+ if (!name) return;
250
+ if (isFixed) {
251
+ const vi = $(this).closest('li').find('.omron-var-value');
252
+ vars.push({ name, value: vi.typedInput('value'), valueType: vi.typedInput('type') });
253
+ } else {
254
+ vars.push({ name });
255
+ }
256
+ });
257
+ this.variables = vars;
258
+ },
259
+ });
260
+ </script>
261
+
262
+ <script type="text/html" data-template-name="omron-write">
263
+ <div class="omron-banner">
264
+ <i class="fa fa-info-circle"></i>
265
+ <b>Deploy after editing.</b> Writes run against the deployed flow. After adding/changing the
266
+ PLC config or variables, click <b>Deploy</b> before testing or using <b>Connect &amp; validate</b>.
267
+ </div>
268
+
269
+ <div class="omron-section-title">Connection</div>
270
+ <div class="form-row">
271
+ <label for="node-input-name">
272
+ <i class="fa fa-tag"></i> Name
273
+ <i class="fa fa-info-circle omron-info" title="A nickname for this write block, shown under its icon on the workspace (the editing canvas). It is purely cosmetic and only helps you tell blocks apart when you have several. Leave it blank and the block will simply label itself with the tag names it writes."></i>
274
+ </label>
275
+ <input type="text" id="node-input-name" placeholder="(optional label)">
276
+ </div>
277
+ <div class="form-row">
278
+ <label for="node-input-plc">
279
+ <i class="fa fa-server"></i> PLC
280
+ <i class="fa fa-info-circle omron-info" title="Which controller (PLC) this block writes to. A 'PLC connection' is a saved entry that holds the controller's network address; you set it up once and then every read and write block can share it, so you do not re-enter the address each time. Use the dropdown to pick one you already made, the pencil button to edit it, or the plus button to create a new one. This must be filled in — without a PLC chosen, the block cannot write anything."></i>
281
+ </label>
282
+ <input type="text" id="node-input-plc">
283
+ </div>
284
+
285
+ <div class="omron-section-title">Values</div>
286
+ <div class="form-row">
287
+ <label for="node-input-valueSource">
288
+ <i class="fa fa-pencil"></i> Source
289
+ <i class="fa fa-info-circle omron-info" title="Where the value to write comes from. FROM INCOMING MESSAGE: the value is supplied by whatever message arrives at this block — for example a slider on a screen, or an earlier block, sends the number to write. In this mode the rows below list only the tag names (no value box), because the value rides in on the message. FIXED VALUE(S) SET HERE: you type the exact value into this block, and it writes that same value every time it is triggered; the triggering message is only a 'go' signal and its contents are ignored. Rule of thumb: choose FIXED when the value never changes (testing, or forcing a constant), and FROM INCOMING MESSAGE when the value comes from somewhere else and changes."></i>
290
+ </label>
291
+ <select id="node-input-valueSource" style="width:68%;">
292
+ <option value="msg">From incoming message</option>
293
+ <option value="fixed">Fixed value(s) set here</option>
294
+ </select>
295
+ </div>
296
+ <div class="form-row" id="omron-msg-hint">
297
+ <label>&nbsp;</label>
298
+ <small class="form-tips" style="display:inline-block; width:68%; margin:0;">
299
+ One variable: send the value as <code>msg.payload</code>. Multiple: send
300
+ <code>msg.payload = { "VarName": value, ... }</code>. Whole array/struct: send an
301
+ array/object as that variable's value.
302
+ </small>
303
+ </div>
304
+ <div class="form-row">
305
+ <label>&nbsp;</label>
306
+ <button type="button" class="red-ui-button red-ui-button-small" id="omron-typeref-btn"
307
+ title="Click to open a reference table. For every Omron tag type (DINT, INT, REAL, BOOL, STRING, and so on — these are the data types you set in Sysmac Studio) it shows what kind of value to enter here and the range of values it allows. Useful when you are not sure what to type for a particular tag. This just opens a help table; it changes nothing.">
308
+ <i class="fa fa-table"></i> Data type reference
309
+ </button>
310
+ <i class="fa fa-info-circle omron-info" title="Important: the value-type dropdown (number / true-false / text / JSON) describes the kind of value YOU are typing here — it is not the PLC tag type. You do not need to match it to the exact Omron type; the system automatically converts your value to whatever the tag actually is. This table simply shows which choice to use for each Omron type."></i>
311
+ <div id="omron-typeref" style="display:none; margin-top:8px;"></div>
312
+ </div>
313
+
314
+ <div class="omron-section-title">Variables</div>
315
+ <div class="form-row" id="omron-col-header" style="display:none;">
316
+ <label>&nbsp;</label>
317
+ <div style="display:inline-block; width:68%; font-size:11px; color:#999;">
318
+ <span style="display:inline-block; width:48%;">variable name</span>
319
+ <span style="display:inline-block;">value to write</span>
320
+ </div>
321
+ </div>
322
+ <div class="form-row">
323
+ <label style="vertical-align:top;">
324
+ <i class="fa fa-list"></i> Variables
325
+ <i class="fa fa-info-circle omron-info" title="The tags you want to write — one per row. A tag (also called a variable) is a named piece of data inside the controller. Type the name exactly as it is spelled in your Sysmac Studio program (names are case-sensitive). Examples: Setpoint (a single value); MyArray[3] (item number 3 of a list — the number in brackets picks which item); MyArray (a whole list at once); MyStruct.Speed (one named field inside a group — the dot picks the field); MyStruct (a whole group at once). In FIXED mode each row also gets a box to type the value to write. Safety tip: writing a single field (MyStruct.Speed) is safer than writing a whole group at once, because writing the whole group replaces every field in it — including ones you did not mean to change. Tip: click Test Connection below for name suggestions as you type."></i>
326
+ </label>
327
+ <div style="display:inline-block; width:68%;">
328
+ <ol id="node-input-variable-container"></ol>
329
+ <div style="margin-top:6px;">
330
+ <button type="button" class="red-ui-button red-ui-button-small" id="omron-validate-btn"
331
+ title="Click to connect to the controller and pull in the full list of available tags. This turns on name suggestions as you type, and marks each tag you have entered with a green check if it exists on the controller or a red X if the name was not found. You must click Deploy first, because this talks to the live controller.">
332
+ <i class="fa fa-refresh"></i> Test Connection, Load and Validate Published Variables
333
+ </button>
334
+ <i class="fa fa-info-circle omron-info" title="What this button does: it connects to your controller and downloads the names of all tags that have been made available to the network (in Sysmac Studio, tags with Network Publish set to Publish Only or Input/Output). Those names then appear as you-type suggestions in the tag boxes, and each tag you have entered gets checked — a green check means it exists on the controller, a red X means the name was not found (usually a typo or a tag that is not published). Note: you must Deploy first, because the check happens against the live, running controller — not the editor."></i>
335
+ <span id="omron-validate-status" style="margin-left:8px; font-size:12px; color:#888;"></span>
336
+ </div>
337
+ </div>
338
+ </div>
339
+
340
+ <div class="form-row" id="omron-injson-row">
341
+ <label>&nbsp;</label>
342
+ <button type="button" class="red-ui-button red-ui-button-small" id="omron-injson-btn"
343
+ title="Click to see a preview of the message you need to send this node to write your tags, based on the tags you have added. Handy for setting up the inject or other node that feeds this one.">
344
+ <i class="fa fa-sign-in"></i> Show input JSON
345
+ </button>
346
+ <i class="fa fa-info-circle omron-info" title="Shows an example of the message to send this node so it knows what value to write (used when Source is From incoming message). If you first click the Test Connection button, the example fills in with the real kinds of values; otherwise it shows generic placeholders. The Copy button copies the example so you can paste it into an inject node."></i>
347
+ <div id="omron-injson" style="display:none; margin-top:8px;"></div>
348
+ </div>
349
+
350
+ <div class="omron-section-title">Options</div>
351
+ <div class="form-row">
352
+ <label>&nbsp;</label>
353
+ <label style="width:auto;">
354
+ <input type="checkbox" id="node-input-verified" style="width:auto; vertical-align:middle;">
355
+ Verified write
356
+ <i class="fa fa-info-circle omron-info" title="What it does: right after writing, this block reads the value back from the controller and checks it really matches what you sent — and reports an error if it does not. Why bother? Even a normal write already gets an 'accepted' or 'error' reply, which is enough almost every time. Verified write adds protection for the rarer case where the controller says 'accepted' but the stored value still is not what you wanted — for example the controller's own program limits it (you send 5000 but it caps at 4000), or another device changes that tag a split second later. Turn it on for important values like setpoints or recipe parameters where you want proof the value truly landed. Trade-off: one extra read per tag, so a little slower — leave it off for ordinary or very frequent writes. One limit: it only checks the instant right after writing, so a later change cannot be detected."></i>
357
+ </label>
358
+ </div>
359
+
360
+ <div class="omron-section-title">Trigger</div>
361
+ <div class="form-row">
362
+ <label for="node-input-pollMs">
363
+ <i class="fa fa-clock-o"></i> Repeat
364
+ <i class="fa fa-info-circle omron-info" title="Makes this block write by itself, repeatedly, on a timer — so you do not need anything wired into its input. Enter how often to write, in milliseconds: 1000 means once every second, 500 means twice a second. Combined with FIXED values, this can continuously hold a tag at a set value — for example a 'heartbeat' signal or keeping an enable switch on. Leave at 0 to write ONLY when something triggers it. Use this on purpose and sparingly: most writes should happen in response to an event (a button, a change), not constantly on a timer."></i>
365
+ </label>
366
+ <input type="text" id="node-input-pollMs" style="width:120px;" placeholder="0">
367
+ <span style="margin-left:6px;">ms (0 = trigger-driven only)</span>
368
+ </div>
369
+ <div class="form-row">
370
+ <label>&nbsp;</label>
371
+ <label style="width:auto;">
372
+ <input type="checkbox" id="node-input-fireOnDeploy" style="width:auto; vertical-align:middle;">
373
+ Write once shortly after deploy
374
+ <i class="fa fa-info-circle omron-info" title="When ticked, this block writes one time on its own, about 1.5 seconds after you click the Deploy button (or after Node-RED restarts). With FIXED values, it is a simple way to put a tag into a known starting state as soon as everything starts up, without needing anything wired into the input. Leave it unticked if you do not want an automatic write at startup. ('Deploy' is the button that saves and starts your flow running.)"></i>
375
+ </label>
376
+ </div>
377
+ </script>
378
+
379
+ <script type="text/html" data-help-name="omron-write">
380
+ <p>Writes one or more variables (tags) to an Omron NX/NJ controller over EtherNet/IP.</p>
381
+
382
+ <h3>Value source</h3>
383
+ <p><b>From incoming message:</b> values come from the message payload (see Input).<br>
384
+ <b>Fixed value(s) set here:</b> names <i>and</i> values are configured in the node; the
385
+ message only triggers the write. Good for testing/commissioning and forcing constants.</p>
386
+
387
+ <h3>Triggering</h3>
388
+ <p>A write fires when a message arrives. <b>Repeat</b> fires on a timer; <b>Write once after
389
+ deploy</b> fires once at startup. With a fixed value plus a timer/after-deploy trigger, the
390
+ node can write with nothing upstream.</p>
391
+
392
+ <h3>Variables &amp; nested data</h3>
393
+ <ul>
394
+ <li><code>Setpoint</code> — scalar</li>
395
+ <li><code>MyArray</code> (value = array) · <code>MyArray[3]</code> (value = element)</li>
396
+ <li><code>MyStruct</code> (value = object) · <code>MyStruct.Speed</code> (value = scalar)</li>
397
+ <li><code>MyStruct.History[2]</code>, <code>Machine.Axes[2].Position</code>, …</li>
398
+ </ul>
399
+ <p>Writing a whole array or structure in one row is the fastest way to push a lot of data.
400
+ Writing a single member is safer than read-modify-write of a whole structure.</p>
401
+
402
+ <h3>Input (From incoming message)</h3>
403
+ <dl class="message-properties">
404
+ <dt>payload <span class="property-type">object | any</span></dt>
405
+ <dd>
406
+ One variable — bare value: <pre>msg.payload = 1500</pre>
407
+ Multiple — object keyed by name: <pre>msg.payload = { "Setpoint": 1500, "Mode": 3 }</pre>
408
+ Whole structure/array as a value: <pre>msg.payload = { "Recipe": { "Speed": 100, "Steps": [1,2,3] } }</pre>
409
+ </dd>
410
+ </dl>
411
+
412
+ <h3>Outputs</h3>
413
+ <p><b>Port 1 (result):</b> message passes through with <code>msg.payload</code> = the map written.</p>
414
+ <p><b>Port 2 (error):</b> on failure; <code>msg.error</code> names the variable and reason,
415
+ <code>msg.errors</code> has per-variable detail. Also reaches Catch nodes.</p>
416
+ </script>
@@ -0,0 +1,163 @@
1
+ 'use strict';
2
+ /**
3
+ * omron-write — writes one or more Omron tags.
4
+ *
5
+ * Value source (node-level switch):
6
+ * - 'fixed': variable names AND values are configured in the node. The incoming message
7
+ * is only a trigger; its payload is ignored. Good for testing/commissioning and for
8
+ * setting a tag to a known constant.
9
+ * - 'msg': values come from the incoming message payload.
10
+ * single configured variable -> msg.payload is the bare value
11
+ * multiple configured variables-> msg.payload = { VarName: value, ... }
12
+ *
13
+ * Triggers: incoming message always fires a write. Optionally also self-fires on a timer
14
+ * and/or once shortly after deploy (useful for 'fixed' writes with nothing upstream).
15
+ *
16
+ * Output: two ports. Port 1 = success (passes the message through, payload = the map
17
+ * written), port 2 = error.
18
+ */
19
+ const { buildWriteMap, coerceTypedValue, describeError } = require('./common');
20
+
21
+ module.exports = function (RED) {
22
+ function OmronWriteNode(config) {
23
+ RED.nodes.createNode(this, config);
24
+ const node = this;
25
+
26
+ node.plcConfig = RED.nodes.getNode(config.plc);
27
+ node.valueSource = config.valueSource || 'msg'; // 'msg' | 'fixed'
28
+ node.verified = !!config.verified;
29
+ node.pollMs = Number(config.pollMs) > 0 ? Number(config.pollMs) : 0;
30
+ node.fireOnDeploy = !!config.fireOnDeploy;
31
+
32
+ // Configured variable rows. Shape depends on value source:
33
+ // fixed: [{ name, valueType, value }]
34
+ // msg: [{ name }]
35
+ node.rows = Array.isArray(config.variables) ? config.variables : [];
36
+ node.configuredNames = node.rows.map(r => r.name).filter(Boolean);
37
+
38
+ let pollTimer = null;
39
+ let deployTimer = null;
40
+
41
+ if (!node.plcConfig) {
42
+ node.status({ fill: 'red', shape: 'ring', text: 'no PLC config' });
43
+ return;
44
+ }
45
+
46
+ function idleStatus() {
47
+ if (node.plcConfig.connected) node.status({ fill: 'green', shape: 'dot', text: 'connected' });
48
+ else node.status({ fill: 'yellow', shape: 'ring', text: 'connecting…' });
49
+ }
50
+ node.plcConfig.on('plc-state', idleStatus);
51
+ idleStatus();
52
+
53
+ // Resolve the {name: value} map to write for this trigger.
54
+ function resolveWriteMap(msg) {
55
+ if (node.valueSource === 'fixed') {
56
+ const fixedPairs = node.rows
57
+ .filter(r => r.name)
58
+ .map(r => {
59
+ // For msg/flow/global typed values, resolve against the (trigger) message context.
60
+ let raw = r.value;
61
+ if (r.valueType === 'msg' || r.valueType === 'flow' || r.valueType === 'global') {
62
+ raw = RED.util.evaluateNodeProperty(r.value, r.valueType, node, msg || {});
63
+ return { name: r.name, value: raw };
64
+ }
65
+ return { name: r.name, value: coerceTypedValue(r.value, r.valueType) };
66
+ });
67
+ return buildWriteMap({ source: 'fixed', fixedPairs });
68
+ }
69
+ // 'msg'
70
+ const payload = msg ? msg.payload : undefined;
71
+ return buildWriteMap({ source: 'msg', configuredNames: node.configuredNames, payload });
72
+ }
73
+
74
+ async function doWrite(msg, send, done) {
75
+ const controller = node.plcConfig.getController();
76
+ let map;
77
+ try {
78
+ map = resolveWriteMap(msg);
79
+ } catch (err) {
80
+ finishError(err, null, msg, send, done);
81
+ return;
82
+ }
83
+
84
+ const names = Object.keys(map);
85
+ node.status({ fill: 'blue', shape: 'dot', text: `writing ${names.length}…` });
86
+ try {
87
+ await node.plcConfig.whenReady();
88
+ // Write each tag. verifiedWriteVariable writes then reads back to confirm.
89
+ const errors = {};
90
+ let anyErr = false;
91
+ for (const name of names) {
92
+ try {
93
+ if (node.verified) await controller.verifiedWriteVariable(name, map[name]);
94
+ else await controller.writeVariable(name, map[name]);
95
+ } catch (err) {
96
+ anyErr = true;
97
+ errors[name] = describeError(err, name);
98
+ }
99
+ }
100
+
101
+ if (anyErr) {
102
+ const out = msg ? RED.util.cloneMessage(msg) : {};
103
+ // Build a specific message: name each failed variable and why.
104
+ const failedNames = Object.keys(errors);
105
+ const detail = failedNames.map(n => `${n}: ${errors[n]}`).join('; ');
106
+ const summary = failedNames.length === 1
107
+ ? `Write failed — ${detail}`
108
+ : `Write failed for ${failedNames.length} of ${names.length} variables — ${detail}`;
109
+ out.error = summary;
110
+ out.errors = errors;
111
+ out.payload = map;
112
+ // Status badge: name the variable when only one failed, else a count.
113
+ const badge = failedNames.length === 1
114
+ ? `write failed: ${failedNames[0]}`.slice(0, 32)
115
+ : `write failed: ${failedNames.length}/${names.length}`;
116
+ node.status({ fill: 'red', shape: 'dot', text: badge });
117
+ send([null, out]);
118
+ if (done) done(new Error(summary));
119
+ else node.error(summary, out);
120
+ return;
121
+ }
122
+
123
+ const out = msg ? RED.util.cloneMessage(msg) : {};
124
+ out.payload = map; // echo what was written
125
+ node.status({ fill: 'green', shape: 'dot', text: `wrote ${names.length} ok` });
126
+ send([out, null]);
127
+ if (done) done();
128
+ } catch (err) {
129
+ finishError(err, names[0], msg, send, done);
130
+ }
131
+ }
132
+
133
+ function finishError(err, varName, msg, send, done) {
134
+ const text = describeError(err, varName);
135
+ node.status({ fill: 'red', shape: 'dot', text: text.slice(0, 32) });
136
+ const errMsg = msg ? RED.util.cloneMessage(msg) : {};
137
+ errMsg.error = text;
138
+ send([null, errMsg]);
139
+ if (done) done(err);
140
+ else node.error(text, errMsg);
141
+ }
142
+
143
+ node.on('input', function (msg, send, done) {
144
+ send = send || function (...a) { node.send(...a); };
145
+ doWrite(msg, send, done);
146
+ });
147
+
148
+ if (node.pollMs > 0) {
149
+ pollTimer = setInterval(() => doWrite(null, (arr) => node.send(arr), null), node.pollMs);
150
+ }
151
+ if (node.fireOnDeploy) {
152
+ deployTimer = setTimeout(() => doWrite(null, (arr) => node.send(arr), null), 1500);
153
+ }
154
+
155
+ node.on('close', function () {
156
+ if (pollTimer) clearInterval(pollTimer);
157
+ if (deployTimer) clearTimeout(deployTimer);
158
+ node.plcConfig.removeListener('plc-state', idleStatus);
159
+ });
160
+ }
161
+
162
+ RED.nodes.registerType('omron-write', OmronWriteNode);
163
+ };
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "node-red-contrib-omron-eip",
3
+ "version": "0.2.0",
4
+ "description": "Node-RED nodes for reading and writing Omron NX/NJ Sysmac controller tags over EtherNet/IP, built on the omron-eip library.",
5
+ "keywords": [
6
+ "node-red",
7
+ "omron",
8
+ "ethernet-ip",
9
+ "ethernet/ip",
10
+ "cip",
11
+ "plc",
12
+ "nx102",
13
+ "nx1p2",
14
+ "nx502",
15
+ "nj101",
16
+ "sysmac",
17
+ "automation"
18
+ ],
19
+ "license": "GPL-2.0",
20
+ "author": "ucsballen",
21
+ "homepage": "https://github.com/ucsballen/omron-eip#readme",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/ucsballen/omron-eip.git"
25
+ },
26
+ "bugs": {
27
+ "url": "https://github.com/ucsballen/omron-eip/issues"
28
+ },
29
+ "node-red": {
30
+ "version": ">=3.0.0",
31
+ "nodes": {
32
+ "omron-plc": "nodes/omron-plc.js",
33
+ "omron-read": "nodes/omron-read.js",
34
+ "omron-write": "nodes/omron-write.js"
35
+ }
36
+ },
37
+ "engines": {
38
+ "node": ">=16"
39
+ },
40
+ "dependencies": {
41
+ "omron-eip": "^0.2.6"
42
+ },
43
+ "files": [
44
+ "nodes/",
45
+ "README.md",
46
+ "MANUAL.md",
47
+ "LICENSE"
48
+ ],
49
+ "scripts": {
50
+ "test": "node test/parse.test.js"
51
+ }
52
+ }