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,299 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('omron-read', {
3
+ category: 'Omron',
4
+ color: '#7fb3d5',
5
+ defaults: {
6
+ name: { value: '' },
7
+ plc: { value: '', type: 'omron-plc', required: true },
8
+ variables: { value: [] },
9
+ varProp: { value: 'variables' },
10
+ outputShape: { value: 'object' },
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-download',
18
+ paletteLabel: 'omron read',
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 read';
23
+ if (names.length <= 2) return 'read: ' + names.join(', ');
24
+ return 'read: ' + names.length + ' tags';
25
+ },
26
+ oneditprepare: function () {
27
+ const node = this;
28
+ $('#node-input-variable-container').css('min-height', '120px').editableList({
29
+ addItem: function (row, index, data) {
30
+ data = data || {};
31
+ $(row).css({ display: 'flex', 'align-items': 'center' });
32
+ const input = $('<input/>', { type: 'text', class: 'omron-var-name',
33
+ placeholder: 'e.g. Counter · MyStruct.Speed · Arr[3] · WholeArray · WholeStruct' })
34
+ .css({ flex: 1, 'margin-right': '6px' }).appendTo(row);
35
+ input.val(data.name || '');
36
+ // If tags were already loaded this session, give new rows autocomplete too.
37
+ if (node._omronTags && node._omronTags.length) {
38
+ input[0].setAttribute('list', 'omron-tags-datalist');
39
+ }
40
+ $('<span/>', { class: 'omron-var-badge' }).css({ width: '18px', 'text-align': 'center' }).appendTo(row);
41
+ },
42
+ removable: true, sortable: true,
43
+ });
44
+ (this.variables || []).forEach(v => $('#node-input-variable-container').editableList('addItem', v));
45
+
46
+ $('#omron-validate-btn').on('click', function () {
47
+ const cfgId = $('#node-input-plc').val();
48
+ const statusEl = $('#omron-validate-status');
49
+ if (!cfgId || cfgId === '_ADD_') {
50
+ statusEl.html('<i class="fa fa-exclamation-triangle"></i> Select & deploy a PLC config first.').css('color', '#b30000');
51
+ return;
52
+ }
53
+ statusEl.html('<i class="fa fa-spinner fa-spin"></i> Connecting…').css('color', '#888');
54
+ $.getJSON('omron-eip/' + cfgId + '/tags')
55
+ .done(function (data) {
56
+ const tags = data.tags || [];
57
+ statusEl.html('<i class="fa fa-check"></i> ' + tags.length + ' tags loaded').css('color', '#3a7d3a');
58
+ $('#node-input-variable-container').find('.omron-var-name').each(function () {
59
+ const base = $(this).val().split(/[.\[]/)[0];
60
+ const badge = $(this).siblings('.omron-var-badge');
61
+ if (!base) { badge.text(''); return; }
62
+ 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 is readable."></i>');
63
+ 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>');
64
+ });
65
+ let dl = $('#omron-tags-datalist');
66
+ if (dl.length === 0) dl = $('<datalist id="omron-tags-datalist"></datalist>').appendTo('body');
67
+ dl.empty();
68
+ tags.forEach(t => dl.append($('<option>').attr('value', t)));
69
+ // Remember tags (and types, if returned) so rows added later get autocomplete
70
+ // and the "Show output JSON" button can use real types.
71
+ node._omronTags = tags;
72
+ node._omronTypes = data.types || {};
73
+ // Attach the list and "nudge" each input so the browser shows the dropdown
74
+ // affordance immediately (without this, the arrow only appears after the value
75
+ // changes, i.e. type-then-delete).
76
+ $('#node-input-variable-container').find('.omron-var-name').each(function () {
77
+ this.setAttribute('list', 'omron-tags-datalist');
78
+ // Dispatching an input event registers the freshly-attached datalist.
79
+ this.dispatchEvent(new Event('input', { bubbles: true }));
80
+ });
81
+ })
82
+ .fail(function (xhr) {
83
+ const e = (xhr.responseJSON && xhr.responseJSON.error) || 'connection failed';
84
+ statusEl.html('<i class="fa fa-times"></i> ' + e).css('color', '#b30000');
85
+ });
86
+ });
87
+
88
+ // ---- Show output JSON ----
89
+ // Returns an example value for a tag based on its known type category, or a generic
90
+ // placeholder if types haven't been loaded (validate not yet pressed).
91
+ function exampleValue(name) {
92
+ const types = node._omronTypes || {};
93
+ const base = name.split(/[.\[]/)[0];
94
+ const cat = types[name] || types[base];
95
+ // If the name targets an element/member, a known whole-array/struct type doesn't apply.
96
+ const isIndexedOrMember = /[.\[]/.test(name);
97
+ switch (cat) {
98
+ case 'bool': return false;
99
+ case 'float': return 0.0;
100
+ case 'number': return 0;
101
+ case 'string': return '';
102
+ case 'datetime': return '<date/time>';
103
+ case 'array': return isIndexedOrMember ? 0 : [];
104
+ case 'struct': return isIndexedOrMember ? 0 : {};
105
+ default: return '<value>'; // unknown / not validated
106
+ }
107
+ }
108
+
109
+ function currentNames() {
110
+ const names = [];
111
+ $('#node-input-variable-container').find('.omron-var-name').each(function () {
112
+ const n = $(this).val().trim();
113
+ if (n) names.push(n);
114
+ });
115
+ return names;
116
+ }
117
+
118
+ function buildOutputJson() {
119
+ const names = currentNames();
120
+ const validated = node._omronTypes && Object.keys(node._omronTypes).length > 0;
121
+ const shape = $('#node-input-outputShape').val();
122
+ let html = '';
123
+ if (names.length === 0) {
124
+ return '<div class="omron-typeref-note">Add one or more variables first.</div>';
125
+ }
126
+ if (shape === 'perTag') {
127
+ // One message per tag.
128
+ const first = names[0];
129
+ const example = { topic: first, payload: exampleValue(first) };
130
+ html += '<div class="omron-typeref-note">This node emits <b>one message per tag</b>. ' +
131
+ 'Each message looks like:</div>';
132
+ 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;">' + JSON.stringify(example, null, 2) + '</pre>';
133
+ if (names.length > 1) {
134
+ html += '<div class="omron-typeref-note">…and similarly for: <code>' +
135
+ names.slice(1).join('</code>, <code>') + '</code>.</div>';
136
+ }
137
+ } else {
138
+ const obj = {};
139
+ names.forEach(n => { obj[n] = exampleValue(n); });
140
+ html += '<div class="omron-typeref-note">This node emits <b>one message</b> with ' +
141
+ '<code>msg.payload</code> =</div>';
142
+ 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;">' + JSON.stringify(obj, null, 2) + '</pre>';
143
+ }
144
+ if (!validated) {
145
+ html += '<div class="omron-typeref-note"><i class="fa fa-info-circle"></i> ' +
146
+ 'Values shown are generic placeholders. Press <b>Test Connection, Load and ' +
147
+ 'Validate Published Variables</b> to fill in the actual types ' +
148
+ '(numbers, booleans, strings, arrays, structures).</div>';
149
+ }
150
+ html += '<button type="button" class="red-ui-button red-ui-button-small omron-copy-btn">' +
151
+ '<i class="fa fa-copy"></i> Copy</button>';
152
+ return html;
153
+ }
154
+
155
+ $('#omron-outjson-btn').on('click', function () {
156
+ const box = $('#omron-outjson');
157
+ box.html(buildOutputJson());
158
+ box.toggle();
159
+ box.find('.omron-copy-btn').on('click', function () {
160
+ const txt = box.find('pre.omron-json').first().text();
161
+ navigator.clipboard && navigator.clipboard.writeText(txt);
162
+ $(this).html('<i class="fa fa-check"></i> Copied');
163
+ setTimeout(() => $(this).html('<i class="fa fa-copy"></i> Copy'), 1500);
164
+ });
165
+ });
166
+ },
167
+ oneditsave: function () {
168
+ const vars = [];
169
+ $('#node-input-variable-container').find('.omron-var-name').each(function () {
170
+ const name = $(this).val().trim();
171
+ if (name) vars.push({ name });
172
+ });
173
+ this.variables = vars;
174
+ },
175
+ });
176
+ </script>
177
+
178
+ <script type="text/html" data-template-name="omron-read">
179
+ <div class="omron-banner">
180
+ <i class="fa fa-info-circle"></i>
181
+ <b>Deploy after editing.</b> Reads run against the deployed flow. After adding/changing the
182
+ PLC config or variables, click <b>Deploy</b> before testing or using <b>Connect &amp; validate</b>.
183
+ </div>
184
+
185
+ <div class="omron-section-title">Connection</div>
186
+ <div class="form-row">
187
+ <label for="node-input-name">
188
+ <i class="fa fa-tag"></i> Name
189
+ <i class="fa fa-info-circle omron-info" title="A nickname for this read 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 reads."></i>
190
+ </label>
191
+ <input type="text" id="node-input-name" placeholder="(optional label)">
192
+ </div>
193
+ <div class="form-row">
194
+ <label for="node-input-plc">
195
+ <i class="fa fa-server"></i> PLC
196
+ <i class="fa fa-info-circle omron-info" title="Which controller (PLC) this block reads from. 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 read anything."></i>
197
+ </label>
198
+ <input type="text" id="node-input-plc">
199
+ </div>
200
+
201
+ <div class="omron-section-title">Variables</div>
202
+ <div class="form-row">
203
+ <label style="vertical-align:top;">
204
+ <i class="fa fa-list"></i> Variables
205
+ <i class="fa fa-info-circle omron-info" title="The tags you want to read — one per row. A tag (also called a variable) is a named piece of data inside the controller, like a counter or a temperature. Type the name exactly as it is spelled in your Sysmac Studio program (names are case-sensitive). Examples of what you can type: Counter (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). Tip: typing just the name of a whole list or group, with nothing in brackets or after a dot, reads all of it in one shot — the fastest way to read a lot of data. Tip: click the Test Connection button below to get name suggestions as you type."></i>
206
+ </label>
207
+ <div style="display:inline-block; width:68%;">
208
+ <ol id="node-input-variable-container"></ol>
209
+ <div style="margin-top:6px;">
210
+ <button type="button" class="red-ui-button red-ui-button-small" id="omron-validate-btn"
211
+ 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.">
212
+ <i class="fa fa-refresh"></i> Test Connection, Load and Validate Published Variables
213
+ </button>
214
+ <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>
215
+ <span id="omron-validate-status" style="margin-left:8px; font-size:12px; color:#888;"></span>
216
+ </div>
217
+ </div>
218
+ </div>
219
+
220
+ <div class="omron-section-title">Output</div>
221
+ <div class="form-row">
222
+ <label for="node-input-outputShape">
223
+ <i class="fa fa-sign-out"></i> Shape
224
+ <i class="fa fa-info-circle omron-info" title="How the readings are packaged and passed to the next block in your flow. (A 'message' is the packet of data that travels along the wires between blocks.) ONE MESSAGE: every tag you read is bundled together into a single message, as a list of name-and-value pairs — best when you want all the readings together, for example to show on a dashboard or save to a log. ONE MESSAGE PER TAG: each tag is sent as its own separate message, with the tag name attached as a label called the 'topic' — best when the next block expects one value at a time, such as a charting block. If you are not sure, leave it on ONE MESSAGE."></i>
225
+ </label>
226
+ <select id="node-input-outputShape" style="width:68%;">
227
+ <option value="object">one message: payload = { name: value }</option>
228
+ <option value="perTag">one message per tag (topic = name)</option>
229
+ </select>
230
+ </div>
231
+ <div class="form-row">
232
+ <label for="node-input-varProp">
233
+ <i class="fa fa-code"></i> Msg override
234
+ <i class="fa fa-info-circle omron-info" title="ADVANCED — most people can leave this exactly as it is. Normally this block reads the fixed list of tags you typed above. This setting lets a different part of your flow decide, on the fly, which tags to read instead — by including the tag name(s) inside the incoming message. This box just names which field of the incoming message to look in (the default, variables, means it looks at msg.variables). If that field is present when a message arrives, those tags are read; if it is absent, the list above is used as normal. Example use: a dropdown menu on a screen lets an operator choose which tags to read, and this block reads whatever they picked. If none of that applies to you, ignore this box."></i>
235
+ </label>
236
+ <input type="text" id="node-input-varProp" style="width:68%;" placeholder="variables">
237
+ </div>
238
+ <div class="form-row">
239
+ <label>&nbsp;</label>
240
+ <button type="button" class="red-ui-button red-ui-button-small" id="omron-outjson-btn"
241
+ title="Click to see a preview of the data this block will send out, based on the tags you have added and the Shape you chose above. This is just a helpful preview — it does not read the controller or change anything. It is handy when setting up the blocks that come after this one, so you know what their incoming data will look like.">
242
+ <i class="fa fa-sign-out"></i> Show output JSON
243
+ </button>
244
+ <i class="fa fa-info-circle omron-info" title="Shows an example of the data this block produces, so you can see exactly what the next block will receive. If you click the Test Connection button first, the example uses the real kinds of values your tags hold (whole numbers, true/false, text, lists, groups). If you have not connected yet, it shows generic placeholders like <value> instead. The Copy button copies the example text so you can paste it into another block. This preview is informational only — it does not contact the controller."></i>
245
+ <div id="omron-outjson" style="display:none; margin-top:8px;"></div>
246
+ </div>
247
+
248
+ <div class="omron-section-title">Trigger</div>
249
+ <div class="form-row">
250
+ <label for="node-input-pollMs">
251
+ <i class="fa fa-clock-o"></i> Repeat
252
+ <i class="fa fa-info-circle omron-info" title="Makes this block read by itself, repeatedly, on a timer — so you do not need anything wired into its input to make it read. Enter how often to read, in milliseconds: 1000 means once every second, 500 means twice a second, 250 means four times a second. (It will also still read any time a message arrives at its input.) Leave this at 0 to read ONLY when something triggers it. Tip: do not set this faster than you truly need — very small numbers send a flood of requests and can overload the controller. For example, reading 100 tags every 250 ms is comfortable on most controllers."></i>
253
+ </label>
254
+ <input type="text" id="node-input-pollMs" style="width:120px;" placeholder="0">
255
+ <span style="margin-left:6px;">ms (0 = message-driven only)</span>
256
+ </div>
257
+ <div class="form-row">
258
+ <label>&nbsp;</label>
259
+ <label style="width:auto;">
260
+ <input type="checkbox" id="node-input-fireOnDeploy" style="width:auto; vertical-align:middle;">
261
+ Read once shortly after deploy
262
+ <i class="fa fa-info-circle omron-info" title="When ticked, this block reads its tags one time on its own, about 1.5 seconds after you click the Deploy button (or after Node-RED restarts). This is handy for grabbing an initial value as soon as everything starts up, instead of waiting for the first trigger or timer. Leave it unticked if you do not need a reading at startup. ('Deploy' is the button that saves and starts your flow running.)"></i>
263
+ </label>
264
+ </div>
265
+ </script>
266
+
267
+ <script type="text/html" data-help-name="omron-read">
268
+ <p>Reads one or more variables (tags) from an Omron NX/NJ controller over EtherNet/IP.</p>
269
+
270
+ <h3>Triggering</h3>
271
+ <p>A read fires when a message arrives. <b>Repeat</b> adds timer-based self-polling, and
272
+ <b>Read once after deploy</b> fetches an initial value at startup. These can be combined.</p>
273
+
274
+ <h3>Variables</h3>
275
+ <p>One row per tag. Symbolic names support the full address syntax:</p>
276
+ <ul>
277
+ <li><code>Counter</code> — a scalar</li>
278
+ <li><code>MyArray</code> — a whole array · <code>MyArray[3]</code> — one element</li>
279
+ <li><code>MyStruct</code> — a whole structure · <code>MyStruct.Speed</code> — a member</li>
280
+ <li><code>MyStruct.History[2]</code> — element of an array inside a structure</li>
281
+ <li><code>Machine.Axes[2].Position</code> — member of a structure inside an array</li>
282
+ </ul>
283
+ <p>Reading a whole array or structure in one row is the fastest way to move a lot of data.
284
+ Click <b>Connect &amp; validate</b> to load live tag names (autocomplete + ✓/✗ checking).</p>
285
+
286
+ <h3>Dynamic variables</h3>
287
+ <p>Override the configured list per message via the property named in <b>Msg override</b>
288
+ (default <code>msg.variables</code>): a tag name or an array of names.</p>
289
+
290
+ <h3>Outputs</h3>
291
+ <p><b>Port 1 (result):</b></p>
292
+ <dl class="message-properties">
293
+ <dt>payload <span class="property-type">object | any</span></dt>
294
+ <dd>One-message mode: <code>{ "Counter": 42, "Speed": 1500 }</code>. Per-tag mode: the value, with <code>msg.topic</code> = name.</dd>
295
+ <dt>errors <span class="property-type">object</span></dt>
296
+ <dd>Present only if some tags in a batch failed: <code>{ name: reason }</code>.</dd>
297
+ </dl>
298
+ <p><b>Port 2 (error):</b> emitted on outright failure; reason on <code>msg.error</code>. Also reaches Catch nodes.</p>
299
+ </script>
@@ -0,0 +1,135 @@
1
+ 'use strict';
2
+ /**
3
+ * omron-read — reads one or more Omron tags.
4
+ *
5
+ * Triggers: an incoming message always fires a read. Optionally also self-polls on a timer,
6
+ * and/or fires once shortly after deploy.
7
+ *
8
+ * Variables: configured as a list in the edit panel, overridable per-message via
9
+ * msg.variables (string or string[]).
10
+ *
11
+ * Output: two ports. Port 1 = success, port 2 = error.
12
+ * - 'object' shape (default): one message, msg.payload = { name: value, ... }.
13
+ * msg.topic = the variable name when exactly one was read. Per-tag failures (when some
14
+ * tags exist and others don't) appear on msg.errors = { name: reason }.
15
+ * - 'perTag' shape: one message per variable, msg.topic = name, msg.payload = value.
16
+ */
17
+ const { resolveReadVariables, shapeReadResult, describeError } = require('./common');
18
+
19
+ module.exports = function (RED) {
20
+ function OmronReadNode(config) {
21
+ RED.nodes.createNode(this, config);
22
+ const node = this;
23
+
24
+ node.plcConfig = RED.nodes.getNode(config.plc);
25
+ node.varProp = config.varProp || 'variables'; // msg property for overrides
26
+ node.outputShape = config.outputShape || 'object'; // 'object' | 'perTag'
27
+ node.names = (config.variables || []).map(v => (typeof v === 'string' ? v : v.name)).filter(Boolean);
28
+ node.pollMs = Number(config.pollMs) > 0 ? Number(config.pollMs) : 0;
29
+ node.fireOnDeploy = !!config.fireOnDeploy;
30
+
31
+ let pollTimer = null;
32
+ let deployTimer = null;
33
+
34
+ if (!node.plcConfig) {
35
+ node.status({ fill: 'red', shape: 'ring', text: 'no PLC config' });
36
+ return;
37
+ }
38
+
39
+ // Reflect connection state on the status badge when idle.
40
+ function idleStatus() {
41
+ if (node.plcConfig.connected) node.status({ fill: 'green', shape: 'dot', text: 'connected' });
42
+ else node.status({ fill: 'yellow', shape: 'ring', text: 'connecting…' });
43
+ }
44
+ node.plcConfig.on('plc-state', idleStatus);
45
+ idleStatus();
46
+
47
+ async function doRead(msg, send, done) {
48
+ const controller = node.plcConfig.getController();
49
+ const names = resolveReadVariables(node.names, msg ? msg[node.varProp] : undefined);
50
+
51
+ if (names.length === 0) {
52
+ const err = new Error('No variables to read (none configured and none on the message).');
53
+ finishError(err, null, msg, send, done);
54
+ return;
55
+ }
56
+
57
+ node.status({ fill: 'blue', shape: 'dot', text: `reading ${names.length}…` });
58
+ try {
59
+ await node.plcConfig.whenReady();
60
+ // partial:true so per-tag errors come back inline rather than rejecting the whole call.
61
+ const resultMap = await controller.readVariables(names, { mode: 'auto', partial: true });
62
+ const shaped = shapeReadResult(resultMap, node.outputShape);
63
+
64
+ if (shaped.kind === 'perTag') {
65
+ for (const item of shaped.items) {
66
+ const base = msg ? RED.util.cloneMessage(msg) : {};
67
+ if (item.error) {
68
+ base.error = describeError(item.error, item.topic);
69
+ base.topic = item.topic;
70
+ send([null, base]);
71
+ } else {
72
+ base.topic = item.topic;
73
+ base.payload = item.payload;
74
+ send([base, null]);
75
+ }
76
+ }
77
+ } else {
78
+ const item = shaped.items[0];
79
+ const out = msg ? RED.util.cloneMessage(msg) : {};
80
+ out.payload = item.payload;
81
+ if (item.topic !== undefined) out.topic = item.topic;
82
+ if (item.errors) out.errors = item.errors;
83
+ send([out, null]);
84
+ }
85
+
86
+ const anyErr = shaped.items.some(i => i.error) ||
87
+ (shaped.items[0] && shaped.items[0].errors);
88
+ if (anyErr) node.status({ fill: 'yellow', shape: 'dot', text: 'read w/ some errors' });
89
+ else node.status({ fill: 'green', shape: 'dot', text: `read ${names.length} ok` });
90
+ if (done) done();
91
+ } catch (err) {
92
+ finishError(err, names[0], msg, send, done);
93
+ }
94
+ }
95
+
96
+ function finishError(err, varName, msg, send, done) {
97
+ const text = describeError(err, varName);
98
+ node.status({ fill: 'red', shape: 'dot', text: text.slice(0, 32) });
99
+ const errMsg = msg ? RED.util.cloneMessage(msg) : {};
100
+ errMsg.error = text;
101
+ errMsg.payload = null;
102
+ send([null, errMsg]);
103
+ if (done) done(err); // also surfaces to Catch nodes
104
+ else node.error(text, errMsg);
105
+ }
106
+
107
+ node.on('input', function (msg, send, done) {
108
+ // Node-RED 1.0+ always provides send/done; guard for older cores just in case.
109
+ send = send || function (...a) { node.send(...a); };
110
+ doRead(msg, send, done);
111
+ });
112
+
113
+ // Self-poll: fire reads on a timer with no upstream node.
114
+ if (node.pollMs > 0) {
115
+ pollTimer = setInterval(() => {
116
+ doRead(null, (arr) => node.send(arr), null);
117
+ }, node.pollMs);
118
+ }
119
+
120
+ // Fire once shortly after deploy (give the connection a moment to come up).
121
+ if (node.fireOnDeploy) {
122
+ deployTimer = setTimeout(() => {
123
+ doRead(null, (arr) => node.send(arr), null);
124
+ }, 1500);
125
+ }
126
+
127
+ node.on('close', function () {
128
+ if (pollTimer) clearInterval(pollTimer);
129
+ if (deployTimer) clearTimeout(deployTimer);
130
+ node.plcConfig.removeListener('plc-state', idleStatus);
131
+ });
132
+ }
133
+
134
+ RED.nodes.registerType('omron-read', OmronReadNode);
135
+ };