node-red-contrib-my-tools 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.
Files changed (67) hide show
  1. package/README.txt +69 -0
  2. package/node-red-contrib-mysql-extended-test/mysql-extended.html +443 -0
  3. package/node-red-contrib-mysql-extended-test/mysql-extended.js +355 -0
  4. package/node-red-contrib-rfid-helper/rfid-lazy.html +153 -0
  5. package/node-red-contrib-rfid-helper/rfid-lazy.js +361 -0
  6. package/node-red-contrib-s7-plus/LICENSE +235 -0
  7. package/node-red-contrib-s7-plus/README.md +141 -0
  8. package/node-red-contrib-s7-plus/examples/read-multiple-values-flow.json +346 -0
  9. package/node-red-contrib-s7-plus/examples/read-write-test-flow.json +172 -0
  10. package/node-red-contrib-s7-plus/examples/write-single-values-flow.json +5583 -0
  11. package/node-red-contrib-s7-plus/lib/s7plus/browse/README.md +72 -0
  12. package/node-red-contrib-s7-plus/lib/s7plus/browse/areas.js +36 -0
  13. package/node-red-contrib-s7-plus/lib/s7plus/browse/datatypes.js +91 -0
  14. package/node-red-contrib-s7-plus/lib/s7plus/browse/flat-browser.js +433 -0
  15. package/node-red-contrib-s7-plus/lib/s7plus/browse/index.js +22 -0
  16. package/node-red-contrib-s7-plus/lib/s7plus/browse/lazy.js +397 -0
  17. package/node-red-contrib-s7-plus/lib/s7plus/browse/node-id.js +22 -0
  18. package/node-red-contrib-s7-plus/lib/s7plus/browse/resolve-symbolic.js +181 -0
  19. package/node-red-contrib-s7-plus/lib/s7plus/browse/vte-helpers.js +49 -0
  20. package/node-red-contrib-s7-plus/lib/s7plus/buffer-stream.js +96 -0
  21. package/node-red-contrib-s7-plus/lib/s7plus/client.js +1238 -0
  22. package/node-red-contrib-s7-plus/lib/s7plus/constants.js +155 -0
  23. package/node-red-contrib-s7-plus/lib/s7plus/crc/index.js +3 -0
  24. package/node-red-contrib-s7-plus/lib/s7plus/crc/s7-crc32.js +70 -0
  25. package/node-red-contrib-s7-plus/lib/s7plus/crc/symbol-crc.js +271 -0
  26. package/node-red-contrib-s7-plus/lib/s7plus/debug.js +88 -0
  27. package/node-red-contrib-s7-plus/lib/s7plus/explore-result.js +41 -0
  28. package/node-red-contrib-s7-plus/lib/s7plus/item-address.js +55 -0
  29. package/node-red-contrib-s7-plus/lib/s7plus/pdu-explore.js +77 -0
  30. package/node-red-contrib-s7-plus/lib/s7plus/pdu-messages.js +359 -0
  31. package/node-red-contrib-s7-plus/lib/s7plus/pobject.js +197 -0
  32. package/node-red-contrib-s7-plus/lib/s7plus/pvalue-codec.js +435 -0
  33. package/node-red-contrib-s7-plus/lib/s7plus/pvalue.js +653 -0
  34. package/node-red-contrib-s7-plus/lib/s7plus/pvar-lists.js +336 -0
  35. package/node-red-contrib-s7-plus/lib/s7plus/read-result.js +91 -0
  36. package/node-red-contrib-s7-plus/lib/s7plus/s7p.js +266 -0
  37. package/node-red-contrib-s7-plus/lib/s7plus/tag-routing.js +30 -0
  38. package/node-red-contrib-s7-plus/lib/s7plus/transport/cotp-stream.js +335 -0
  39. package/node-red-contrib-s7-plus/lib/s7plus/transport/s7-transport.js +335 -0
  40. package/node-red-contrib-s7-plus/nodes/icons/s7complus.png +0 -0
  41. package/node-red-contrib-s7-plus/nodes/icons/s7complus.svg +17 -0
  42. package/node-red-contrib-s7-plus/nodes/s7complus-endpoint.js +825 -0
  43. package/node-red-contrib-s7-plus/nodes/s7complus-explore.js +91 -0
  44. package/node-red-contrib-s7-plus/nodes/s7complus-in.js +255 -0
  45. package/node-red-contrib-s7-plus/nodes/s7complus-out.js +182 -0
  46. package/node-red-contrib-s7-plus/nodes/s7complus.html +1541 -0
  47. package/node-red-contrib-s7-plus/nodes/s7complus.js +10 -0
  48. package/node-red-contrib-s7-plus/package.json +50 -0
  49. package/node-red-contrib-s7-plus/plc/s7-1500/DB_Arrays.db +17 -0
  50. package/node-red-contrib-s7-plus/plc/s7-1500/DB_Binary.db +16 -0
  51. package/node-red-contrib-s7-plus/plc/s7-1500/DB_BitStrings.db +36 -0
  52. package/node-red-contrib-s7-plus/plc/s7-1500/DB_CharacterStrings.db +42 -0
  53. package/node-red-contrib-s7-plus/plc/s7-1500/DB_DateAndTime.db +70 -0
  54. package/node-red-contrib-s7-plus/plc/s7-1500/DB_FloatingPoint.db +42 -0
  55. package/node-red-contrib-s7-plus/plc/s7-1500/DB_Integers.db +72 -0
  56. package/node-red-contrib-s7-plus/plc/s7-1500/DB_Timers.db +40 -0
  57. package/node-red-contrib-s7-plus/scripts/example-flow-shared.js +39 -0
  58. package/node-red-contrib-s7-plus/scripts/generate-read-multiple-flow.js +331 -0
  59. package/node-red-contrib-s7-plus/scripts/generate-rw-test-flow.js +690 -0
  60. package/node-red-contrib-s7-plus/scripts/generate-write-flow.js +476 -0
  61. package/node-red-contrib-s7-plus/scripts/layout-write-flow.js +156 -0
  62. package/node-red-contrib-s7-plus/scripts/run-tests.js +16 -0
  63. package/node-red-flowgobal-change/variable-change.html +425 -0
  64. package/node-red-flowgobal-change/variable-change.js +92 -0
  65. package/node-red-ui_media_viewer/ui_media_viewer.html +87 -0
  66. package/node-red-ui_media_viewer/ui_media_viewer.js +136 -0
  67. package/package.json +12 -0
@@ -0,0 +1,1541 @@
1
+ <script type="text/html" data-template-name="s7-plus endpoint">
2
+ <div class="form-row">
3
+ <label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
4
+ <input type="text" id="node-config-input-name" placeholder="Name">
5
+ </div>
6
+ <div class="form-row">
7
+ <label for="node-config-input-address"><i class="fa fa-globe"></i> Address</label>
8
+ <input type="text" id="node-config-input-address" placeholder="192.168.0.1">
9
+ </div>
10
+ <div class="form-row">
11
+ <label for="node-config-input-timeout"><i class="fa fa-clock-o"></i> Timeout (ms)</label>
12
+ <input type="number" id="node-config-input-timeout" placeholder="10000" min="100">
13
+ </div>
14
+ </script>
15
+
16
+ <script type="text/html" data-help-name="s7-plus endpoint">
17
+ <p>Shared S7CommPlus connection to a Siemens S7-1200/1500 PLC (ISO-on-TCP, port 102).</p>
18
+ <p>Configure IP address and timeout. The endpoint only manages the network session and
19
+ reconnect/watchdog logic — symbols are defined in each <b>S7+ Read</b> / <b>S7+ Write</b>
20
+ node individually.</p>
21
+ <p>On connect, no symbol table is loaded. Symbolic paths are resolved lazily on the first
22
+ read or write (walked along the PLC browse tree) and cached for five minutes. Use the
23
+ <b>S7+ Explore</b> node for a full manual browse.</p>
24
+ <p>Successful connects and unexpected disconnects are written to the Node-RED runtime log.
25
+ Set <code>S7P_DEBUG=endpoint</code> for detailed protocol and reconnect tracing.</p>
26
+ </script>
27
+
28
+ <style id="s7complus-browse-styles">
29
+ .s7complus-browse-body {
30
+ overflow-x: hidden;
31
+ overflow-y: auto;
32
+ width: 100%;
33
+ padding: 8px 0 8px 12px;
34
+ box-sizing: border-box;
35
+ }
36
+ .s7complus-browse-tree {
37
+ list-style: none;
38
+ margin: 0;
39
+ padding: 0;
40
+ font-family: var(--red-ui-primary-font);
41
+ font-size: var(--red-ui-primary-font-size);
42
+ color: var(--red-ui-primary-text-color);
43
+ user-select: none;
44
+ -webkit-user-select: none;
45
+ }
46
+ .s7complus-browse-item {
47
+ line-height: 22px;
48
+ }
49
+ .s7complus-browse-row {
50
+ display: flex;
51
+ align-items: center;
52
+ gap: 4px;
53
+ cursor: pointer;
54
+ border-radius: 2px;
55
+ padding: 0 2px;
56
+ user-select: none;
57
+ -webkit-user-select: none;
58
+ }
59
+ .s7complus-browse-row.selected,
60
+ .s7complus-browse-row.selected span {
61
+ background: #beebff;
62
+ color: #2e3138 !important;
63
+ }
64
+ .s7complus-browse-row.selected .s7complus-browse-chevron {
65
+ color: #2e3138 !important;
66
+ }
67
+ .s7complus-browse-chevron {
68
+ width: 14px;
69
+ display: inline-block;
70
+ flex-shrink: 0;
71
+ color: var(--red-ui-secondary-text-color, #888);
72
+ text-align: center;
73
+ }
74
+ .s7complus-browse-children {
75
+ list-style: none;
76
+ margin: 0;
77
+ padding: 0;
78
+ display: none;
79
+ }
80
+ .s7complus-browse-children.is-open {
81
+ display: block;
82
+ }
83
+ .s7complus-browse-muted {
84
+ color: var(--red-ui-secondary-text-color, #888);
85
+ }
86
+ .s7complus-browse-error {
87
+ color: var(--red-ui-text-color-error, #c00);
88
+ }
89
+ .s7complus-browse-empty {
90
+ margin-left: 14px;
91
+ }
92
+ .s7complus-scope-body {
93
+ overflow-x: hidden;
94
+ overflow-y: auto;
95
+ width: 100%;
96
+ padding: 8px 0 8px 12px;
97
+ box-sizing: border-box;
98
+ }
99
+ .s7complus-scope-list {
100
+ list-style: none;
101
+ margin: 0;
102
+ padding: 0;
103
+ font-family: var(--red-ui-primary-font);
104
+ font-size: var(--red-ui-primary-font-size);
105
+ color: var(--red-ui-primary-text-color);
106
+ }
107
+ .s7complus-scope-item {
108
+ line-height: 24px;
109
+ }
110
+ .s7complus-scope-row {
111
+ display: flex;
112
+ align-items: center;
113
+ gap: 6px;
114
+ padding: 0 2px;
115
+ }
116
+ .s7complus-scope-row label {
117
+ width: auto;
118
+ margin: 0;
119
+ cursor: pointer;
120
+ display: inline-flex;
121
+ align-items: center;
122
+ gap: 6px;
123
+ }
124
+ .s7complus-scope-row input[type="checkbox"] {
125
+ width: auto;
126
+ margin: 0;
127
+ }
128
+ .s7complus-scope-row.is-disabled {
129
+ opacity: 0.45;
130
+ }
131
+ .s7complus-scope-chevron {
132
+ width: 14px;
133
+ display: inline-block;
134
+ flex-shrink: 0;
135
+ color: var(--red-ui-secondary-text-color, #888);
136
+ text-align: center;
137
+ cursor: pointer;
138
+ }
139
+ .s7complus-scope-children {
140
+ list-style: none;
141
+ margin: 0;
142
+ padding: 0 0 0 22px;
143
+ display: none;
144
+ }
145
+ .s7complus-scope-children.is-open {
146
+ display: block;
147
+ }
148
+ .s7complus-scope-hint {
149
+ color: var(--red-ui-secondary-text-color, #888);
150
+ font-size: 12px;
151
+ padding: 4px 0 4px 22px;
152
+ }
153
+ .s7complus-dialog > .ui-dialog-content {
154
+ overflow: hidden !important;
155
+ box-sizing: border-box;
156
+ padding: 0 !important;
157
+ }
158
+ .s7complus-dialog > .ui-dialog-content > div {
159
+ width: 100%;
160
+ height: 100%;
161
+ box-sizing: border-box;
162
+ }
163
+ </style>
164
+
165
+ <script type="text/javascript">
166
+ (function () {
167
+ function s7complusBrowsePayload(ctx, extra) {
168
+ return Object.assign({
169
+ id: ctx.endpointId || undefined,
170
+ address: ctx.address || undefined,
171
+ timeout: ctx.timeout,
172
+ browseSessionId: ctx.browseSessionId || undefined
173
+ }, extra || {});
174
+ }
175
+
176
+ function s7complusBrowseAjax(ctx, path, data) {
177
+ const root = (typeof RED !== 'undefined' && RED.settings && RED.settings.httpAdminRoot) || '/';
178
+ const base = root.endsWith('/') ? root : root + '/';
179
+ return $.ajax({
180
+ url: base + 's7complus/browse/' + path,
181
+ method: 'POST',
182
+ contentType: 'application/json',
183
+ data: JSON.stringify(s7complusBrowsePayload(ctx, data))
184
+ });
185
+ }
186
+
187
+ function s7complusSetupDialogScroll(shell, scrollBody) {
188
+ const dialog = shell.closest('.ui-dialog');
189
+ dialog.addClass('s7complus-dialog');
190
+
191
+ function fitScrollBody() {
192
+ const content = dialog.children('.ui-dialog-content');
193
+ const available = content.innerHeight();
194
+ if (available > 0) {
195
+ shell.css({ width: '100%', height: '100%' });
196
+ scrollBody.css({
197
+ maxHeight: available + 'px',
198
+ height: available + 'px',
199
+ width: '100%'
200
+ });
201
+ }
202
+ }
203
+
204
+ fitScrollBody();
205
+ setTimeout(fitScrollBody, 0);
206
+ return fitScrollBody;
207
+ }
208
+
209
+ function s7complusAddSymbolRow(listEl, row, existing) {
210
+ if (!row || !row.name || !row.address) return false;
211
+ if (existing && existing[row.name]) return false;
212
+ listEl.editableList('addItem', row);
213
+ if (existing) existing[row.name] = true;
214
+ return true;
215
+ }
216
+
217
+ function s7complusEndpointBrowseContext(endpointId) {
218
+ const ep = endpointId ? RED.nodes.node(endpointId) : null;
219
+ return {
220
+ endpointId: endpointId || undefined,
221
+ address: ep ? ep.address : '',
222
+ timeout: ep ? (ep.timeout || 10000) : 10000,
223
+ browseSessionId: null
224
+ };
225
+ }
226
+
227
+ window.s7complusAddSymbolRow = s7complusAddSymbolRow;
228
+ window.s7complusEndpointBrowseContext = s7complusEndpointBrowseContext;
229
+
230
+ window.s7complusOpenBrowseTree = function (options) {
231
+ const ctx = options.context;
232
+ if (!ctx.address || !String(ctx.address).trim()) {
233
+ RED.notify('Enter a PLC address before browsing', 'warning');
234
+ return;
235
+ }
236
+
237
+ let selectedLi = null;
238
+ let selectedRow = null;
239
+ let browseSessionId = ctx.browseSessionId || null;
240
+
241
+ const treeWrap = $('<div/>', { class: 's7complus-browse-body' });
242
+ const treeRoot = $('<ul/>', { class: 's7complus-browse-tree' }).appendTo(treeWrap);
243
+ const shell = $('<div/>', { class: 'hide' }).append(treeWrap).appendTo('body');
244
+
245
+ function setAddButtonDisabled(disabled) {
246
+ shell.closest('.ui-dialog')
247
+ .find('.ui-dialog-buttonpane button')
248
+ .first()
249
+ .prop('disabled', disabled);
250
+ }
251
+
252
+ function deselect() {
253
+ if (selectedRow) selectedRow.removeClass('selected');
254
+ selectedRow = null;
255
+ selectedLi = null;
256
+ }
257
+
258
+ function addSelected() {
259
+ if (!selectedLi) {
260
+ RED.notify('Select a symbol in the tree', 'warning');
261
+ return;
262
+ }
263
+ const node = selectedLi.data('browseNode');
264
+ if (!node) return;
265
+ if (!node.isLeaf) {
266
+ RED.notify('Expand the tree and select a leaf symbol (scalar or array element)', 'warning');
267
+ return;
268
+ }
269
+ setAddButtonDisabled(true);
270
+ s7complusBrowseAjax(ctx, 'resolve', { nodeId: node.id, browseSessionId: browseSessionId })
271
+ .done(function (row) {
272
+ if (options.onResolved) {
273
+ options.onResolved(row);
274
+ }
275
+ RED.notify('Added: ' + row.name, 'success');
276
+ })
277
+ .fail(function (jq) {
278
+ RED.notify('Resolve failed: ' + ((jq.responseJSON && jq.responseJSON.error) || jq.statusText), 'error');
279
+ })
280
+ .always(function () {
281
+ setAddButtonDisabled(false);
282
+ });
283
+ }
284
+
285
+ function renderNode(parentUl, node, depth) {
286
+ const li = $('<li/>', { class: 's7complus-browse-item', css: { marginLeft: (depth * 14) + 'px' } });
287
+ const row = $('<div/>', { class: 's7complus-browse-row' });
288
+ const chevron = $('<span/>', { class: 's7complus-browse-chevron' });
289
+ if (node.hasChildren) chevron.html('<i class="fa fa-caret-right"></i>');
290
+ else chevron.text(' ');
291
+ const labelEl = $('<span/>').text(node.label + (node.datatype ? ' (' + node.datatype + ')' : ''));
292
+ row.append(chevron, labelEl);
293
+ li.append(row);
294
+ parentUl.append(li);
295
+
296
+ const childUl = $('<ul/>', { class: 's7complus-browse-tree s7complus-browse-children' });
297
+ li.append(childUl);
298
+ let loaded = false;
299
+ let expanded = false;
300
+
301
+ function select() {
302
+ if (selectedRow) selectedRow.removeClass('selected');
303
+ selectedRow = row;
304
+ row.addClass('selected');
305
+ selectedLi = li;
306
+ li.data('browseNode', node);
307
+ }
308
+
309
+ row.on('click', function (e) {
310
+ e.stopPropagation();
311
+ if (e.detail > 1) return;
312
+ select();
313
+ });
314
+
315
+ row.on('dblclick', function (e) {
316
+ e.stopPropagation();
317
+ e.preventDefault();
318
+ if (node.isLeaf) {
319
+ select();
320
+ addSelected();
321
+ return;
322
+ }
323
+ if (node.hasChildren) {
324
+ if (selectedRow && selectedRow[0] === row[0]) deselect();
325
+ expand();
326
+ }
327
+ });
328
+
329
+ function expand() {
330
+ if (!node.hasChildren) return;
331
+ if (expanded) {
332
+ expanded = false;
333
+ childUl.removeClass('is-open');
334
+ chevron.html('<i class="fa fa-caret-right"></i>');
335
+ return;
336
+ }
337
+ expanded = true;
338
+ chevron.html('<i class="fa fa-caret-down"></i>');
339
+ if (loaded) {
340
+ childUl.addClass('is-open');
341
+ return;
342
+ }
343
+ chevron.html('<i class="fa fa-spinner fa-spin"></i>');
344
+ s7complusBrowseAjax(ctx, 'children', { nodeId: node.id, browseSessionId: browseSessionId })
345
+ .done(function (res) {
346
+ loaded = true;
347
+ chevron.html('<i class="fa fa-caret-down"></i>');
348
+ (res.nodes || []).forEach(function (ch) {
349
+ renderNode(childUl, ch, depth + 1);
350
+ });
351
+ childUl.addClass('is-open');
352
+ if (!(res.nodes || []).length) {
353
+ childUl.append($('<li/>', { class: 's7complus-browse-muted s7complus-browse-empty' }).text('(empty)'));
354
+ }
355
+ })
356
+ .fail(function (jq) {
357
+ expanded = false;
358
+ chevron.html('<i class="fa fa-caret-right"></i>');
359
+ RED.notify('Browse failed: ' + ((jq.responseJSON && jq.responseJSON.error) || jq.statusText), 'error');
360
+ });
361
+ }
362
+
363
+ chevron.on('click', function (e) {
364
+ e.stopPropagation();
365
+ expand();
366
+ });
367
+ }
368
+
369
+ treeRoot.append($('<li/>', { class: 's7complus-browse-muted' }).text('Loading…'));
370
+
371
+ s7complusBrowseAjax(ctx, 'roots', {})
372
+ .done(function (res) {
373
+ treeRoot.empty();
374
+ browseSessionId = res.browseSessionId || browseSessionId;
375
+ ctx.browseSessionId = browseSessionId;
376
+ (res.nodes || []).forEach(function (n) {
377
+ renderNode(treeRoot, n, 0);
378
+ });
379
+ if (!(res.nodes || []).length) {
380
+ treeRoot.append($('<li/>', { class: 's7complus-browse-muted' }).text('No blocks found'));
381
+ }
382
+ })
383
+ .fail(function (jq) {
384
+ treeRoot.empty();
385
+ treeRoot.append($('<li/>', { class: 's7complus-browse-error' })
386
+ .text((jq.responseJSON && jq.responseJSON.error) || 'Browse failed'));
387
+ });
388
+
389
+ shell.dialog({
390
+ modal: true,
391
+ title: 'Browse PLC — select symbols to add',
392
+ width: 500,
393
+ height: 520,
394
+ resizable: true,
395
+ buttons: [
396
+ {
397
+ text: 'Add selected',
398
+ click: function () {
399
+ addSelected();
400
+ }
401
+ },
402
+ {
403
+ text: 'Close',
404
+ click: function () {
405
+ shell.dialog('close');
406
+ }
407
+ }
408
+ ],
409
+ open: function () {
410
+ s7complusSetupDialogScroll(shell, treeWrap);
411
+ },
412
+ resize: function () {
413
+ s7complusSetupDialogScroll(shell, treeWrap);
414
+ },
415
+ close: function () {
416
+ shell.dialog('destroy').remove();
417
+ }
418
+ });
419
+ };
420
+
421
+ const S7_EXPLORE_MEMORY_AREAS = [
422
+ { name: 'IArea', label: 'IArea (Inputs)' },
423
+ { name: 'QArea', label: 'QArea (Outputs)' },
424
+ { name: 'MArea', label: 'MArea (Merker)' },
425
+ { name: 'S7Timers', label: 'S7Timers (Timers)' },
426
+ { name: 'S7Counters', label: 'S7Counters (Counters)' }
427
+ ];
428
+
429
+ function s7complusNormalizeExploreScope(scope) {
430
+ if (!scope || scope.everything) {
431
+ return { everything: true, dbs: [], areas: [] };
432
+ }
433
+ const dbs = Array.isArray(scope.dbs) ? scope.dbs.filter(Boolean) : [];
434
+ const areas = Array.isArray(scope.areas) ? scope.areas.filter(Boolean) : [];
435
+ if (dbs.length === 0 && areas.length === 0) {
436
+ return { everything: true, dbs: [], areas: [] };
437
+ }
438
+ return { everything: false, dbs: dbs, areas: areas };
439
+ }
440
+
441
+ function s7complusExploreScopeSummary(scope) {
442
+ const s = s7complusNormalizeExploreScope(scope);
443
+ if (s.everything) return 'Everything';
444
+ const parts = [];
445
+ if (s.dbs.length) {
446
+ parts.push(s.dbs.length + ' DB' + (s.dbs.length === 1 ? '' : 's'));
447
+ }
448
+ if (s.areas.length) {
449
+ parts.push(s.areas.join(', '));
450
+ }
451
+ return parts.length ? parts.join(', ') : 'Everything';
452
+ }
453
+
454
+ window.s7complusNormalizeExploreScope = s7complusNormalizeExploreScope;
455
+ window.s7complusExploreScopeSummary = s7complusExploreScopeSummary;
456
+ window.s7complusExploreScopeDraft = { everything: true, dbs: [], areas: [] };
457
+
458
+ window.s7complusOpenExploreScopeDialog = function (options) {
459
+ const ctx = options.context;
460
+ if (!ctx.address || !String(ctx.address).trim()) {
461
+ RED.notify('Enter a PLC address on the endpoint before selecting scope', 'warning');
462
+ return;
463
+ }
464
+
465
+ const initial = s7complusNormalizeExploreScope(options.scope);
466
+ const body = $('<div/>', { class: 's7complus-scope-body' });
467
+ const list = $('<ul/>', { class: 's7complus-scope-list' }).appendTo(body);
468
+ const shell = $('<div/>', { class: 'hide' }).append(body).appendTo('body');
469
+
470
+ const everythingRow = $('<div/>', { class: 's7complus-scope-row' });
471
+ const everythingCb = $('<input/>', { type: 'checkbox', id: 's7complus-scope-everything' });
472
+ everythingRow.append(
473
+ $('<label/>').append(everythingCb, $('<span/>').text('Everything'))
474
+ );
475
+ list.append($('<li/>', { class: 's7complus-scope-item' }).append(everythingRow));
476
+
477
+ const dbItem = $('<li/>', { class: 's7complus-scope-item' });
478
+ const dbRow = $('<div/>', { class: 's7complus-scope-row' });
479
+ const dbChevron = $('<span/>', { class: 's7complus-scope-chevron' })
480
+ .html('<i class="fa fa-caret-right"></i>');
481
+ const dbParentCb = $('<input/>', { type: 'checkbox', id: 's7complus-scope-dbs-parent' });
482
+ const dbLabel = $('<span/>', { class: 's7complus-scope-db-label', css: { cursor: 'pointer' } })
483
+ .text('Data Blocks (DB)');
484
+ dbRow.append(dbChevron, $('<label/>').append(dbParentCb), dbLabel);
485
+ dbItem.append(dbRow);
486
+ const dbChildren = $('<ul/>', { class: 's7complus-scope-children' });
487
+ const dbHint = $('<li/>', { class: 's7complus-scope-hint' }).text('Loading DB list...');
488
+ dbChildren.append(dbHint);
489
+ dbItem.append(dbChildren);
490
+ list.append(dbItem);
491
+
492
+ const areaCbs = {};
493
+ S7_EXPLORE_MEMORY_AREAS.forEach(function (area) {
494
+ const row = $('<div/>', { class: 's7complus-scope-row s7complus-scope-area-row' });
495
+ const cb = $('<input/>', { type: 'checkbox', class: 's7complus-scope-area-cb' });
496
+ row.append($('<label/>').append(cb, $('<span/>').text(area.label)));
497
+ list.append($('<li/>', { class: 's7complus-scope-item s7complus-scope-area-item' }).append(row));
498
+ areaCbs[area.name] = cb;
499
+ });
500
+
501
+ const dbCbs = {};
502
+ const dbItemRows = {};
503
+ let dbExpanded = false;
504
+ let dbFilterValue = '';
505
+
506
+ function setDbExpanded(open) {
507
+ dbExpanded = open;
508
+ dbChildren.toggleClass('is-open', open);
509
+ dbChevron.html(open ? '<i class="fa fa-caret-down"></i>' : '<i class="fa fa-caret-right"></i>');
510
+ }
511
+
512
+ function wildcardToRegExp(pattern) {
513
+ const escaped = String(pattern).replace(/[.+^${}()|[\]\\]/g, '\\$&');
514
+ const expr = escaped.replace(/\*/g, '.*').replace(/\?/g, '.');
515
+ return new RegExp(expr, 'i');
516
+ }
517
+
518
+ function applyDbFilter(pattern) {
519
+ dbFilterValue = pattern || '';
520
+ const trimmed = dbFilterValue.trim();
521
+ const re = trimmed ? wildcardToRegExp(trimmed) : null;
522
+ let visible = 0;
523
+ Object.keys(dbItemRows).forEach(function (name) {
524
+ const checked = dbCbs[name] && dbCbs[name].prop('checked');
525
+ const match = !re || re.test(name) || checked;
526
+ dbItemRows[name].toggle(match);
527
+ if (match) visible++;
528
+ });
529
+ dbChildren.find('.s7complus-scope-db-empty').remove();
530
+ if (re && visible === 0 && Object.keys(dbItemRows).length) {
531
+ dbChildren.append($('<li/>', { class: 's7complus-scope-hint s7complus-scope-db-empty' })
532
+ .text('No DBs match "' + trimmed + '"'));
533
+ }
534
+ }
535
+
536
+ function syncDbParentState() {
537
+ const names = Object.keys(dbCbs);
538
+ if (!names.length) {
539
+ dbParentCb.prop('checked', false).prop('indeterminate', false);
540
+ return;
541
+ }
542
+ const checked = names.filter(function (n) { return dbCbs[n].prop('checked'); });
543
+ if (checked.length === 0) {
544
+ dbParentCb.prop('checked', false).prop('indeterminate', false);
545
+ } else if (checked.length === names.length) {
546
+ dbParentCb.prop('checked', true).prop('indeterminate', false);
547
+ } else {
548
+ dbParentCb.prop('checked', false).prop('indeterminate', true);
549
+ }
550
+ }
551
+
552
+ function setScopeControlsDisabled(disabled) {
553
+ dbRow.toggleClass('is-disabled', disabled);
554
+ dbParentCb.prop('disabled', disabled);
555
+ Object.keys(dbCbs).forEach(function (name) {
556
+ dbCbs[name].prop('disabled', disabled);
557
+ });
558
+ dbChevron.css('pointer-events', disabled ? 'none' : 'auto');
559
+ $('.s7complus-scope-area-row').toggleClass('is-disabled', disabled);
560
+ Object.keys(areaCbs).forEach(function (name) {
561
+ areaCbs[name].prop('disabled', disabled);
562
+ });
563
+ }
564
+
565
+ function applyInitialScope() {
566
+ everythingCb.prop('checked', initial.everything);
567
+ if (initial.everything) {
568
+ setScopeControlsDisabled(true);
569
+ dbParentCb.prop('checked', true).prop('indeterminate', false);
570
+ Object.keys(dbCbs).forEach(function (name) {
571
+ dbCbs[name].prop('checked', true);
572
+ });
573
+ Object.keys(areaCbs).forEach(function (name) {
574
+ areaCbs[name].prop('checked', true);
575
+ });
576
+ return;
577
+ }
578
+ setScopeControlsDisabled(false);
579
+ Object.keys(areaCbs).forEach(function (name) {
580
+ areaCbs[name].prop('checked', initial.areas.indexOf(name) >= 0);
581
+ });
582
+ const dbSet = {};
583
+ initial.dbs.forEach(function (n) { dbSet[n] = true; });
584
+ Object.keys(dbCbs).forEach(function (name) {
585
+ dbCbs[name].prop('checked', !!dbSet[name]);
586
+ });
587
+ syncDbParentState();
588
+ }
589
+
590
+ function collectScope() {
591
+ if (everythingCb.prop('checked')) {
592
+ return { everything: true, dbs: [], areas: [] };
593
+ }
594
+ const dbs = Object.keys(dbCbs).filter(function (name) {
595
+ return dbCbs[name].prop('checked');
596
+ });
597
+ const areas = Object.keys(areaCbs).filter(function (name) {
598
+ return areaCbs[name].prop('checked');
599
+ });
600
+ return s7complusNormalizeExploreScope({ everything: false, dbs: dbs, areas: areas });
601
+ }
602
+
603
+ let savedSelection = null;
604
+
605
+ everythingCb.on('change', function () {
606
+ if (everythingCb.prop('checked')) {
607
+ savedSelection = {
608
+ dbs: Object.keys(dbCbs).filter(function (name) {
609
+ return dbCbs[name].prop('checked');
610
+ }),
611
+ areas: Object.keys(areaCbs).filter(function (name) {
612
+ return areaCbs[name].prop('checked');
613
+ }),
614
+ dbExpanded: dbExpanded
615
+ };
616
+ setScopeControlsDisabled(true);
617
+ setDbExpanded(false);
618
+ dbParentCb.prop('checked', true).prop('indeterminate', false);
619
+ Object.keys(dbCbs).forEach(function (name) {
620
+ dbCbs[name].prop('checked', true);
621
+ });
622
+ Object.keys(areaCbs).forEach(function (name) {
623
+ areaCbs[name].prop('checked', true);
624
+ });
625
+ } else {
626
+ setScopeControlsDisabled(false);
627
+ if (savedSelection) {
628
+ const dbSet = {};
629
+ savedSelection.dbs.forEach(function (n) { dbSet[n] = true; });
630
+ Object.keys(dbCbs).forEach(function (name) {
631
+ dbCbs[name].prop('checked', !!dbSet[name]);
632
+ });
633
+ const areaSet = {};
634
+ savedSelection.areas.forEach(function (n) { areaSet[n] = true; });
635
+ Object.keys(areaCbs).forEach(function (name) {
636
+ areaCbs[name].prop('checked', !!areaSet[name]);
637
+ });
638
+ syncDbParentState();
639
+ setDbExpanded(!!savedSelection.dbExpanded);
640
+ savedSelection = null;
641
+ }
642
+ }
643
+ });
644
+
645
+ dbParentCb.on('change', function () {
646
+ const checked = dbParentCb.prop('checked');
647
+ dbParentCb.prop('indeterminate', false);
648
+ Object.keys(dbCbs).forEach(function (name) {
649
+ dbCbs[name].prop('checked', checked);
650
+ });
651
+ });
652
+
653
+ dbChevron.on('click', function (e) {
654
+ e.stopPropagation();
655
+ setDbExpanded(!dbExpanded);
656
+ });
657
+
658
+ dbLabel.on('click', function (e) {
659
+ e.stopPropagation();
660
+ if (dbLabel.closest('.s7complus-scope-row').hasClass('is-disabled')) return;
661
+ setDbExpanded(!dbExpanded);
662
+ });
663
+
664
+ s7complusBrowseAjax(ctx, 'roots', { browseSessionId: ctx.browseSessionId || null })
665
+ .done(function (res) {
666
+ dbHint.remove();
667
+ const blocks = (res.nodes || []).filter(function (n) { return n.nodeKind === 'block'; });
668
+ if (ctx.browseSessionId !== undefined) {
669
+ ctx.browseSessionId = res.browseSessionId || ctx.browseSessionId;
670
+ }
671
+ if (!blocks.length) {
672
+ dbChildren.append($('<li/>', { class: 's7complus-scope-hint' })
673
+ .text('No DBs found — connect to the PLC to list data blocks'));
674
+ dbParentCb.prop('disabled', true);
675
+ applyInitialScope();
676
+ return;
677
+ }
678
+ blocks.forEach(function (node) {
679
+ const row = $('<div/>', { class: 's7complus-scope-row' });
680
+ const cb = $('<input/>', { type: 'checkbox' });
681
+ row.append($('<label/>').append(cb, $('<span/>').text(node.label)));
682
+ const li = $('<li/>', { class: 's7complus-scope-item' }).append(row);
683
+ dbChildren.append(li);
684
+ dbCbs[node.label] = cb;
685
+ dbItemRows[node.label] = li;
686
+ cb.on('change', function () {
687
+ syncDbParentState();
688
+ applyDbFilter(dbFilterValue);
689
+ });
690
+ });
691
+ applyInitialScope();
692
+ applyDbFilter(dbFilterValue);
693
+ })
694
+ .fail(function (jq) {
695
+ dbHint.text((jq.responseJSON && jq.responseJSON.error) || 'Failed to load DB list');
696
+ dbParentCb.prop('disabled', true);
697
+ applyInitialScope();
698
+ });
699
+
700
+ shell.dialog({
701
+ modal: true,
702
+ title: 'Explore scope — select memory areas',
703
+ width: 460,
704
+ height: 480,
705
+ resizable: true,
706
+ buttons: [
707
+ {
708
+ text: 'OK',
709
+ click: function () {
710
+ const scope = collectScope();
711
+ window.s7complusExploreScopeDraft = scope;
712
+ if (options.onSaved) options.onSaved(scope);
713
+ shell.dialog('close');
714
+ }
715
+ },
716
+ {
717
+ text: 'Cancel',
718
+ click: function () {
719
+ shell.dialog('close');
720
+ }
721
+ }
722
+ ],
723
+ open: function () {
724
+ s7complusSetupDialogScroll(shell, body);
725
+
726
+ const buttonPane = shell.closest('.ui-dialog').find('.ui-dialog-buttonpane');
727
+ buttonPane.css({ display: 'flex', alignItems: 'center', gap: '8px' });
728
+ buttonPane.find('.ui-dialog-buttonset').css({ marginLeft: 'auto', float: 'none' });
729
+
730
+ const filterWrap = $('<div/>', {
731
+ class: 's7complus-scope-filter',
732
+ css: { display: 'flex', alignItems: 'center', gap: '6px' }
733
+ });
734
+ const inputWrap = $('<div/>', {
735
+ css: { position: 'relative', display: 'inline-flex', alignItems: 'center' }
736
+ });
737
+ const filterInput = $('<input/>', {
738
+ type: 'text',
739
+ placeholder: 'DB-Filter',
740
+ css: { width: '180px', margin: 0, paddingRight: '22px', boxSizing: 'border-box' }
741
+ }).val(dbFilterValue);
742
+ const clearBtn = $('<span/>', {
743
+ title: 'Clear filter',
744
+ css: {
745
+ position: 'absolute',
746
+ right: '6px',
747
+ top: '50%',
748
+ transform: 'translateY(-50%)',
749
+ lineHeight: '1',
750
+ cursor: 'pointer',
751
+ color: 'var(--red-ui-secondary-text-color, #888)',
752
+ display: dbFilterValue ? 'inline' : 'none'
753
+ }
754
+ }).html('<i class="fa fa-times-circle"></i>');
755
+ function refreshFilter() {
756
+ if (!dbExpanded) setDbExpanded(true);
757
+ clearBtn.css('display', filterInput.val() ? 'inline' : 'none');
758
+ applyDbFilter(filterInput.val());
759
+ }
760
+ filterInput.on('input', refreshFilter);
761
+ clearBtn.on('click', function () {
762
+ filterInput.val('');
763
+ refreshFilter();
764
+ filterInput.trigger('focus');
765
+ });
766
+ inputWrap.append(filterInput, clearBtn);
767
+ filterWrap.append(inputWrap);
768
+ buttonPane.prepend(filterWrap);
769
+ },
770
+ resize: function () {
771
+ s7complusSetupDialogScroll(shell, body);
772
+ },
773
+ close: function () {
774
+ shell.dialog('destroy').remove();
775
+ }
776
+ });
777
+ };
778
+ })();
779
+
780
+ RED.nodes.registerType('s7-plus endpoint', {
781
+ category: 'config',
782
+ paletteLabel: 'S7+ Endpoint',
783
+ defaults: {
784
+ name: { value: '' },
785
+ address: { value: '', required: true },
786
+ timeout: { value: 10000 }
787
+ },
788
+ label: function () {
789
+ return this.name || ('S7+ ' + this.address);
790
+ },
791
+ oneditprepare: function () {
792
+ $('#node-config-input-timeout').val(this.timeout || 10000);
793
+ }
794
+ });
795
+ </script>
796
+
797
+ <script type="text/html" data-template-name="s7-plus read">
798
+ <div class="form-row">
799
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
800
+ <input type="text" id="node-input-name" placeholder="Name">
801
+ </div>
802
+ <div class="form-row">
803
+ <label for="node-input-endpoint"><i class="fa fa-server"></i> Endpoint</label>
804
+ <input type="text" id="node-input-endpoint">
805
+ </div>
806
+ <div class="form-row">
807
+ <label for="node-input-readMode"><i class="fa fa-clock-o"></i> 觸發模式</label>
808
+ <select id="node-input-readMode" style="width: 130px;">
809
+ <option value="manual">手動 (Inject)</option>
810
+ <option value="auto">自動 (Cyclic)</option>
811
+ </select>
812
+ <span id="cycle-time-row" style="display:none; margin-left:10px;">
813
+ <input type="number" id="node-input-readCycle" style="width: 80px;" placeholder="1000"> ms
814
+ </span>
815
+ </div>
816
+ <div class="form-row">
817
+ <label>&nbsp;</label>
818
+ <input type="checkbox" id="node-input-diff" style="display:inline-block; width:auto; vertical-align:top;">
819
+ <label for="node-input-diff" style="width:70%;">Diff Filter (僅在數值改變時輸出)</label>
820
+ </div>
821
+ <div class="form-row">
822
+ <label><i class="fa fa-database"></i> Symbols</label>
823
+ <button type="button" id="s7complus-in-pick-btn" class="red-ui-button">
824
+ <i class="fa fa-sitemap"></i> Pick from PLC
825
+ </button>
826
+ <span id="s7complus-in-symbols-count" style="margin-left:8px;color:#888;"></span>
827
+ </div>
828
+ <div class="form-row node-input-symbols-row">
829
+ <ol id="node-input-in-symbols"></ol>
830
+ </div>
831
+ <div class="form-row">
832
+ <label for="node-input-outputFormat"><i class="fa fa-sign-out"></i> Output Format</label>
833
+ <select id="node-input-outputFormat">
834
+ <option value="object">Object</option>
835
+ <option value="array">Array</option>
836
+ <option value="value">Value (單一數值)</option> </select>
837
+ </select>
838
+ </div>
839
+ </script>
840
+
841
+ <script type="text/html" data-help-name="s7-plus read">
842
+ <p>Reads one or more PLC symbols from a Siemens S7-1200/1500 via the configured
843
+ <b>S7+ Endpoint</b>. Every incoming message triggers exactly one read.</p>
844
+
845
+ <p>Which symbols are read is normally configured on the node with the
846
+ <b>Pick from PLC</b> button. The results are returned in <code>msg.payload</code>.</p>
847
+
848
+ <h3>Inputs</h3>
849
+ <p>Every incoming message triggers exactly one read. You do not need to set
850
+ <code>msg.payload</code> — an inject with an empty payload is enough.</p>
851
+ <dl class="message-properties">
852
+ <dt class="optional">symbols <span class="property-type">string[]</span></dt>
853
+ <dd>Optional. <b>Replaces</b> the symbols configured on the node for this
854
+ one message (it does not add to them).
855
+ <ul>
856
+ <li>If <code>msg.symbols</code> is set, the node reads <b>only</b>
857
+ those symbols and <b>ignores</b> the ones configured in the editor.</li>
858
+ <li>If <code>msg.symbols</code> is not set, the node uses the
859
+ symbols configured in the editor (the normal case).</li>
860
+ <li>Only an <b>array of strings</b> is accepted, e.g.
861
+ <code>[ "Motor.speed", "Pump.on" ]</code>. Any other type
862
+ (objects, single string, etc.) is rejected with an error.</li>
863
+ </ul></dd>
864
+
865
+ <dt class="optional">addSymbols <span class="property-type">string[]</span></dt>
866
+ <dd>Optional. A list of additional symbol paths to read on top of the symbols
867
+ from the editor or from <code>msg.symbols</code>, e.g.
868
+ <code>msg.addSymbols = ["DB1.speed", "DB2.temp[0]"]</code>.
869
+ <ul>
870
+ <li>The list is <b>merged</b> with those symbols (it adds to them, it
871
+ does not replace them).</li>
872
+ <li>A symbol that appears in both is read only once.</li>
873
+ <li>Only symbolic path strings are supported (same as names from
874
+ <b>S7+ Explore</b> or <b>Pick from PLC</b>).</li>
875
+ </ul>
876
+ Leave <code>msg.addSymbols</code> unset to read only the base symbol list.</dd>
877
+ </dl>
878
+
879
+ <h3>Outputs</h3>
880
+ <p>The <b>Output Format</b> option on the node controls how results are returned in
881
+ <code>msg.payload</code>:</p>
882
+ <ul>
883
+ <li><b>Object</b> (default) — one property per symbol, keyed by symbol name.</li>
884
+ <li><b>Array</b> — an array of objects, each with <code>symbol</code>,
885
+ <code>value</code>, <code>status</code>, and <code>error</code>.</li>
886
+ </ul>
887
+ <dl class="message-properties">
888
+ <dt>payload <span class="property-type">object | array</span></dt>
889
+ <dd>
890
+ <p><b>Object format:</b> keyed by symbol name, with one entry per read symbol.
891
+ Each entry is an object with:</p>
892
+ <ul>
893
+ <li><code>value</code> – the decoded value on success, or <code>null</code> on error</li>
894
+ <li><code>status</code> – <code>"ok"</code> or <code>"error"</code></li>
895
+ <li><code>error</code> – error text, empty string on success</li>
896
+ </ul>
897
+ <p><b>Array format:</b> same fields per symbol, plus <code>symbol</code> with the
898
+ symbol name. Evaluate with <code>msg.payload.find(p =&gt; p.symbol === '...')</code>
899
+ or loop over the array.</p>
900
+ </dd>
901
+ </dl>
902
+ <p>Per-symbol read errors are <b>not</b> fatal: the message is still sent and the node
903
+ turns yellow. With object format, evaluate <code>msg.payload[name].status</code> downstream;
904
+ with array format, check each item's <code>status</code>. Only a connection
905
+ or protocol failure aborts the whole read: the node turns red, no message is sent, and the
906
+ error is raised so it can be handled by the flow's error handling.</p>
907
+
908
+ <h3>Details</h3>
909
+ <p>Symbols are picked from the live PLC symbol tree via <b>Pick from PLC</b>; each row
910
+ shows its <code>name</code> (the output key) and datatype. Symbols are re-resolved
911
+ symbolically at runtime, so the correct datatype is determined automatically &mdash; you
912
+ never set it by hand.</p>
913
+ <p>Resolved symbols are cached for 5 minutes, so repeated reads are fast. If the PLC
914
+ program changes, the mismatch is detected automatically and the symbols are re-resolved.</p>
915
+ <p>While a read is still running, further incoming messages are skipped (node status
916
+ <i>"skipped (busy)"</i>) to protect the connection from overload.</p>
917
+
918
+ <h3>Example</h3>
919
+ <p>Read the configured symbols (any incoming message triggers the read).
920
+ Result in <code>msg.payload</code> (object format):</p>
921
+ <pre>{
922
+ "Motor.speed": { "value": 1450, "status": "ok", "error": "" },
923
+ "Tank.level": { "value": 73.2, "status": "ok", "error": "" }
924
+ }</pre>
925
+ <p>Same read with array format:</p>
926
+ <pre>[
927
+ { "symbol": "Motor.speed", "value": 1450, "status": "ok", "error": "" },
928
+ { "symbol": "Tank.level", "value": 73.2, "status": "ok", "error": "" }
929
+ ]</pre>
930
+ <p>Read additional ad-hoc symbols without configuring them, by setting:</p>
931
+ <pre>msg.addSymbols = ["DB1.temperature"]</pre>
932
+ <p>After <b>S7+ Explore</b>, copy the browse result into <code>msg.addSymbols</code>
933
+ before read (e.g. in a <b>function</b> node):</p>
934
+ <pre>msg.addSymbols = msg.payload;
935
+ return msg;</pre>
936
+ </script>
937
+
938
+ <script type="text/javascript">
939
+ (function () {
940
+ function buildSymbolsEditableList(listSelector, countSelector) {
941
+ const list = $(listSelector);
942
+ list.editableList({
943
+ addItem: function (container, index, data) {
944
+ const row = $('<div/>', { style: 'display:flex;gap:6px;align-items:center;' }).appendTo(container);
945
+
946
+ // 1. 自訂名稱 (Alias) - 移除 readonly,讓使用者可以隨意修改
947
+ $('<input/>', { type: 'text', class: 'node-input-s7sym-name', placeholder: '自訂名稱 (Alias)' })
948
+ .css('flex', '2 1 0')
949
+ .css('min-width', '0')
950
+ .val(data ? data.name : '')
951
+ .attr('title', '程式呼叫時使用的名稱')
952
+ .appendTo(row);
953
+
954
+ // 2. PLC 實際路徑 - 改為顯示出來但設定為唯讀,讓你知道它對應哪個點位
955
+ $('<input/>', { type: 'text', class: 'node-input-s7sym-address', placeholder: 'PLC Address' })
956
+ .css('flex', '3 1 0')
957
+ .css('min-width', '0')
958
+ .val(data ? data.address : '')
959
+ .prop('readonly', true)
960
+ .attr('title', 'PLC 實際路徑 (不可改)')
961
+ .appendTo(row);
962
+
963
+ // 3. 資料型態
964
+ $('<input/>', { type: 'text', class: 'node-input-s7sym-datatype', placeholder: 'Datatype' })
965
+ .css('flex', '0 0 100px')
966
+ .css('min-width', '0')
967
+ .prop('readonly', true)
968
+ .val(data ? (data.datatype || '') : '')
969
+ .appendTo(row);
970
+ },
971
+ addButton: false,
972
+ removable: true,
973
+ sortable: true
974
+ });
975
+ function refreshCount() {
976
+ let n = 0;
977
+ list.editableList('items').each(function () { n++; });
978
+ if (countSelector) {
979
+ $(countSelector).text(n ? (n + ' symbol' + (n === 1 ? '' : 's')) : '');
980
+ }
981
+ }
982
+ list.on('change input', refreshCount);
983
+ return { list: list, refreshCount: refreshCount };
984
+ }
985
+
986
+ function collectExistingNames(list) {
987
+ const existing = {};
988
+ list.editableList('items').each(function () {
989
+ const n = $(this).find('.node-input-s7sym-name').val();
990
+ if (n) existing[n] = true;
991
+ });
992
+ return existing;
993
+ }
994
+
995
+ function serializeSymbols(list) {
996
+ const rows = [];
997
+ list.editableList('items').each(function () {
998
+ const name = $(this).find('.node-input-s7sym-name').val();
999
+ const address = $(this).find('.node-input-s7sym-address').val();
1000
+ const datatype = $(this).find('.node-input-s7sym-datatype').val();
1001
+ if (name && address) {
1002
+ rows.push({ name: name, address: address, datatype: datatype || undefined });
1003
+ }
1004
+ });
1005
+ return rows;
1006
+ }
1007
+
1008
+ function wirePickButton(pickBtnSelector, listGetter, countRefresh) {
1009
+ $(pickBtnSelector).on('click', function (e) {
1010
+ e.preventDefault();
1011
+ e.stopPropagation();
1012
+ const epId = $('#node-input-endpoint').val();
1013
+ if (!epId) {
1014
+ RED.notify('Select an endpoint first', 'warning');
1015
+ return;
1016
+ }
1017
+ const ctx = window.s7complusEndpointBrowseContext(epId);
1018
+ if (!ctx.address || !String(ctx.address).trim()) {
1019
+ RED.notify('Configure the endpoint PLC address first', 'warning');
1020
+ return;
1021
+ }
1022
+ const list = listGetter();
1023
+ const existing = collectExistingNames(list);
1024
+ window.s7complusOpenBrowseTree({
1025
+ context: ctx,
1026
+ onResolved: function (row) {
1027
+ if (window.s7complusAddSymbolRow(list, row, existing)) {
1028
+ countRefresh();
1029
+ }
1030
+ }
1031
+ });
1032
+ });
1033
+ }
1034
+
1035
+ window.s7complusSymListSerialize = serializeSymbols;
1036
+ window.s7complusSymListBuild = buildSymbolsEditableList;
1037
+ window.s7complusSymListWirePick = wirePickButton;
1038
+
1039
+ function prepareOutputFormat(node) {
1040
+ $('#node-input-outputFormat').val(node.outputFormat || 'object');
1041
+ }
1042
+
1043
+ function saveOutputFormat(node) {
1044
+ node.outputFormat = $('#node-input-outputFormat').val() || 'object';
1045
+ }
1046
+
1047
+ window.s7complusPrepareOutputFormat = prepareOutputFormat;
1048
+ window.s7complusSaveOutputFormat = saveOutputFormat;
1049
+ })();
1050
+
1051
+ RED.nodes.registerType('s7-plus read', {
1052
+ category: 'S7 Plus',
1053
+ color: '#3FADB5',
1054
+ paletteLabel: 'S7+ Read',
1055
+ defaults: {
1056
+ name: { value: '' },
1057
+ endpoint: { value: '', type: 's7-plus endpoint', required: true },
1058
+ symbols: { value: [] },
1059
+ outputFormat: { value: 'object' },
1060
+ // 加入新的預設值
1061
+ readMode: { value: 'manual' },
1062
+ readCycle: { value: 1000 },
1063
+ diff: { value: false }
1064
+ },
1065
+ inputs: 1,
1066
+ outputs: 1,
1067
+ icon: 's7complus.svg',
1068
+ label: function () {
1069
+ if (this.name) return this.name;
1070
+ if (Array.isArray(this.symbols) && this.symbols.length) {
1071
+ return this.symbols.length === 1
1072
+ ? ('S7+ Read ' + this.symbols[0].name)
1073
+ : ('S7+ Read [' + this.symbols.length + ']');
1074
+ }
1075
+ return 'S7+ Read';
1076
+ },
1077
+ labelStyle: function () {
1078
+ return this.name ? 'node_label_italic' : '';
1079
+ },
1080
+ oneditprepare: function () {
1081
+ // 加入 UI 連動邏輯:選擇自動時才顯示時間輸入框
1082
+ $('#node-input-readMode').on('change', function() {
1083
+ if ($(this).val() === 'auto') {
1084
+ $('#cycle-time-row').show();
1085
+ } else {
1086
+ $('#cycle-time-row').hide();
1087
+ }
1088
+ });
1089
+ $('#node-input-readMode').val(this.readMode || 'manual').trigger('change');
1090
+ $('#node-input-readCycle').val(this.readCycle || 1000);
1091
+ $('#node-input-diff').prop('checked', this.diff);
1092
+
1093
+ const ctrl = window.s7complusSymListBuild('#node-input-in-symbols', '#s7complus-in-symbols-count');
1094
+ const formatSelect = $('#node-input-outputFormat');
1095
+
1096
+ // --- 新增:獨立的選項更新與預設邏輯 ---
1097
+ function updateFormatOptions(isUserAction) {
1098
+ let count = 0;
1099
+ ctrl.list.editableList('items').each(function () { count++; });
1100
+
1101
+ const valueOption = formatSelect.find('option[value="value"]');
1102
+ if (count === 1) {
1103
+ // 1 個點位:顯示 Value 選項
1104
+ valueOption.show().prop('disabled', false);
1105
+
1106
+ // 只要確定是「人為操作 (增刪點位)」造成的,毫不猶豫切成 value
1107
+ if (isUserAction) {
1108
+ formatSelect.val('value');
1109
+ }
1110
+ } else {
1111
+ // 超過 1 個或沒有點位:隱藏 Value 選項
1112
+ valueOption.hide().prop('disabled', true);
1113
+
1114
+ // 防呆:如果原本是 value,強制切回 object 避免出錯
1115
+ if (formatSelect.val() === 'value') {
1116
+ formatSelect.val('object');
1117
+ }
1118
+ }
1119
+ }
1120
+
1121
+ // 1. 先載入使用者原本存檔的點位列表 (此時不觸發跳轉)
1122
+ const table = Array.isArray(this.symbols) ? this.symbols : [];
1123
+ table.forEach(function (row) { ctrl.list.editableList('addItem', row); });
1124
+
1125
+ // 2. 載入使用者原本存檔的 OutputFormat,避免舊設定被洗掉
1126
+ window.s7complusPrepareOutputFormat(this);
1127
+
1128
+ // 3. 根據剛才載入的數量,把 UI 隱藏/顯示設定好 (isUserAction = false,所以不會亂蓋掉)
1129
+ updateFormatOptions(false);
1130
+
1131
+ // 4. 終極解法:啟動 DOM 監聽器!
1132
+ // 確保接下來只要清單裡有 `<li>` 的新增或刪除,絕對是使用者的操作
1133
+ const observer = new MutationObserver(function(mutations) {
1134
+ updateFormatOptions(true);
1135
+ ctrl.refreshCount(); // 順便更新總數顯示
1136
+ });
1137
+ // 監聽點位列表容器的子元素變動
1138
+ observer.observe(ctrl.list[0], { childList: true });
1139
+
1140
+ // 綁定「Pick from PLC」按鈕
1141
+ window.s7complusSymListWirePick('#s7complus-in-pick-btn', function () { return ctrl.list; }, ctrl.refreshCount);
1142
+ },
1143
+ oneditsave: function () {
1144
+ this.symbols = window.s7complusSymListSerialize($('#node-input-in-symbols'));
1145
+ window.s7complusSaveOutputFormat(this);
1146
+ }
1147
+ });
1148
+ </script>
1149
+
1150
+ <script type="text/html" data-template-name="s7-plus write">
1151
+ <div class="form-row">
1152
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
1153
+ <input type="text" id="node-input-name" placeholder="Name">
1154
+ </div>
1155
+ <div class="form-row">
1156
+ <label for="node-input-endpoint"><i class="fa fa-server"></i> Endpoint</label>
1157
+ <input type="text" id="node-input-endpoint">
1158
+ </div>
1159
+ <div class="form-row">
1160
+ <label><i class="fa fa-database"></i> Symbols</label>
1161
+ <button type="button" id="s7complus-out-pick-btn" class="red-ui-button">
1162
+ <i class="fa fa-sitemap"></i> Pick from PLC
1163
+ </button>
1164
+ <span id="s7complus-out-symbols-count" style="margin-left:8px;color:#888;"></span>
1165
+ </div>
1166
+ <div class="form-row node-input-symbols-row">
1167
+ <ol id="node-input-out-symbols"></ol>
1168
+ </div>
1169
+ <div class="form-row">
1170
+ <label for="node-input-outputFormat"><i class="fa fa-sign-out"></i> Output Format</label>
1171
+ <select id="node-input-outputFormat">
1172
+ <option value="object">Object</option>
1173
+ <option value="array">Array</option>
1174
+ </select>
1175
+ </div>
1176
+ </script>
1177
+
1178
+ <script type="text/html" data-help-name="s7-plus write">
1179
+ <p>Writes one or more PLC symbols on a Siemens S7-1200/1500 via the configured
1180
+ <b>S7+ Endpoint</b>. Every incoming message triggers exactly one write.</p>
1181
+
1182
+ <p>Which symbols are written is normally configured on the node with the
1183
+ <b>Pick from PLC</b> button. Override the target list per message with
1184
+ <code>msg.symbols</code> (top-level). The values to write come from
1185
+ <code>msg.payload</code>.</p>
1186
+
1187
+ <h3>Inputs</h3>
1188
+ <p>Every incoming message triggers exactly one write. Unlike <b>S7+ Read</b>,
1189
+ <code>msg.payload</code> must contain the value(s) to write &mdash; an empty
1190
+ payload is not enough.</p>
1191
+ <dl class="message-properties">
1192
+ <dt>payload <span class="property-type">object | scalar</span></dt>
1193
+ <dd><b>Input:</b> the value(s) to write. The format depends on how many symbols
1194
+ the node writes:
1195
+ <ul>
1196
+ <li><b>Exactly one symbol:</b> put the value straight into
1197
+ <code>msg.payload</code>. Example for the value 1500:
1198
+ <code>msg.payload = 1500</code>.</li>
1199
+ <li><b>Two or more symbols:</b> <code>msg.payload</code> must be an
1200
+ object. Each key is the symbol name, each value is what to write to
1201
+ it. Example:
1202
+ <code>msg.payload = { "Motor.speed": 1500, "Pump.on": true }</code>.</li>
1203
+ </ul>
1204
+ A value is always assigned to the symbol whose name matches its key.</dd>
1205
+
1206
+ <dt class="optional">symbols <span class="property-type">array</span></dt>
1207
+ <dd>Optional. <b>Replaces</b> the symbols configured on the node for this
1208
+ one message (it does not add to them).
1209
+ <ul>
1210
+ <li>If <code>msg.symbols</code> is set, the node writes <b>only</b>
1211
+ those symbols and <b>ignores</b> the ones configured in the editor.</li>
1212
+ <li>If <code>msg.symbols</code> is not set, the node uses the
1213
+ symbols configured in the editor (the normal case).</li>
1214
+ </ul>
1215
+ Provide a list of symbol names, e.g.
1216
+ <code>[ "Motor.speed", "Pump.on" ]</code> (objects like
1217
+ <code>{ "name": "Motor.speed" }</code> also work). The values for these
1218
+ symbols still come from <code>msg.payload</code>, using the same rules
1219
+ as above.</dd>
1220
+ </dl>
1221
+
1222
+ <h3>Outputs</h3>
1223
+ <p>The node <b>replaces</b> <code>msg.payload</code> with the write result. If you
1224
+ need the original input value downstream, copy it to another property (e.g. with a
1225
+ <b>change</b> node) before the write.</p>
1226
+ <p>The <b>Output Format</b> option on the node controls how results are returned in
1227
+ <code>msg.payload</code>:</p>
1228
+ <ul>
1229
+ <li><b>Object</b> (default) — one property per symbol, keyed by symbol name.</li>
1230
+ <li><b>Array</b> — an array of objects, each with <code>symbol</code>,
1231
+ <code>value</code>, <code>status</code>, and <code>error</code>.</li>
1232
+ </ul>
1233
+ <dl class="message-properties">
1234
+ <dt>payload <span class="property-type">object | array</span></dt>
1235
+ <dd>
1236
+ <p><b>Object format:</b> keyed by symbol name, with one entry per written symbol.
1237
+ Each entry is an object with:</p>
1238
+ <ul>
1239
+ <li><code>value</code> – the written value on success, or <code>null</code> on error</li>
1240
+ <li><code>status</code> – <code>"ok"</code> or <code>"error"</code></li>
1241
+ <li><code>error</code> – error text, empty string on success</li>
1242
+ </ul>
1243
+ <p><b>Array format:</b> same fields per symbol, plus <code>symbol</code> with the
1244
+ symbol name. Evaluate with <code>msg.payload.find(p =&gt; p.symbol === '...')</code>
1245
+ or loop over the array.</p>
1246
+ </dd>
1247
+ </dl>
1248
+ <p>Per-symbol write errors are <b>not</b> fatal: the message is still sent and the node
1249
+ turns yellow. With object format, evaluate <code>msg.payload[name].status</code> downstream;
1250
+ with array format, check each item's <code>status</code>. Only a connection
1251
+ or protocol failure aborts the whole write: the node turns red, no message is sent, and the
1252
+ error is raised so it can be handled by the flow's error handling.</p>
1253
+
1254
+ <h3>Details</h3>
1255
+ <p>Symbols are picked from the live PLC symbol tree via <b>Pick from PLC</b>; each row
1256
+ shows its <code>name</code> and datatype. Symbols are re-resolved symbolically at runtime,
1257
+ so the correct datatype is determined automatically &mdash; you never set it by hand or
1258
+ via message properties.</p>
1259
+ <p>While a write is still running, further incoming messages are skipped (node status
1260
+ <i>"skipped (busy)"</i>) to protect the connection from overload.</p>
1261
+
1262
+ <h3>Example</h3>
1263
+ <p>Write a single configured symbol <code>Motor.speed</code>:</p>
1264
+ <pre>msg.payload = 1500</pre>
1265
+ <p>Write several symbols at once:</p>
1266
+ <pre>msg.payload = {
1267
+ "Motor.speed": 1500,
1268
+ "Pump.on": true
1269
+ }</pre>
1270
+ <p>Result in <code>msg.payload</code> after the write (object format):</p>
1271
+ <pre>{
1272
+ "Motor.speed": { "value": 1500, "status": "ok", "error": "" },
1273
+ "Pump.on": { "value": true, "status": "ok", "error": "" }
1274
+ }</pre>
1275
+ <p>Same write with array format:</p>
1276
+ <pre>[
1277
+ { "symbol": "Motor.speed", "value": 1500, "status": "ok", "error": "" },
1278
+ { "symbol": "Pump.on", "value": true, "status": "ok", "error": "" }
1279
+ ]</pre>
1280
+ <p>Override symbols for one message (values still from <code>msg.payload</code>):</p>
1281
+ <pre>msg.symbols = ["Pump.on"];
1282
+ msg.payload = true;
1283
+ return msg;</pre>
1284
+ </script>
1285
+
1286
+ <script type="text/javascript">
1287
+ RED.nodes.registerType('s7-plus write', {
1288
+ category: 'S7 Plus',
1289
+ color: '#3FADB5',
1290
+ paletteLabel: 'S7+ Write',
1291
+ defaults: {
1292
+ name: { value: '' },
1293
+ endpoint: { value: '', type: 's7-plus endpoint', required: true },
1294
+ symbols: { value: [] },
1295
+ outputFormat: { value: 'object' }
1296
+ },
1297
+ inputs: 1,
1298
+ outputs: 1,
1299
+ icon: 's7complus.svg',
1300
+ label: function () {
1301
+ if (this.name) return this.name;
1302
+ if (Array.isArray(this.symbols) && this.symbols.length) {
1303
+ return this.symbols.length === 1
1304
+ ? ('S7+ Write ' + this.symbols[0].name)
1305
+ : ('S7+ Write [' + this.symbols.length + ']');
1306
+ }
1307
+ return 'S7+ Write';
1308
+ },
1309
+ labelStyle: function () {
1310
+ return this.name ? 'node_label_italic' : '';
1311
+ },
1312
+ oneditprepare: function () {
1313
+ const ctrl = window.s7complusSymListBuild('#node-input-out-symbols', '#s7complus-out-symbols-count');
1314
+ const table = Array.isArray(this.symbols) ? this.symbols : [];
1315
+ table.forEach(function (row) { ctrl.list.editableList('addItem', row); });
1316
+ ctrl.refreshCount();
1317
+ window.s7complusSymListWirePick('#s7complus-out-pick-btn', function () { return ctrl.list; }, ctrl.refreshCount);
1318
+ window.s7complusPrepareOutputFormat(this);
1319
+ },
1320
+ oneditsave: function () {
1321
+ this.symbols = window.s7complusSymListSerialize($('#node-input-out-symbols'));
1322
+ window.s7complusSaveOutputFormat(this);
1323
+ }
1324
+ });
1325
+ </script>
1326
+
1327
+ <script type="text/html" data-template-name="s7-plus explore">
1328
+ <div class="form-row">
1329
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
1330
+ <input type="text" id="node-input-name" placeholder="Name">
1331
+ </div>
1332
+ <div class="form-row">
1333
+ <label for="node-input-endpoint"><i class="fa fa-server"></i> Endpoint</label>
1334
+ <input type="text" id="node-input-endpoint">
1335
+ </div>
1336
+ <div class="form-row">
1337
+ <label><i class="fa fa-filter"></i> Scope</label>
1338
+ <button type="button" id="s7complus-explore-scope-btn" class="red-ui-button">
1339
+ <i class="fa fa-check-square-o"></i> Explore scope
1340
+ </button>
1341
+ <span id="s7complus-explore-scope-summary" style="margin-left:8px;color:#888;"></span>
1342
+ </div>
1343
+ <div class="form-row">
1344
+ <label for="node-input-symbolInfos"><i class="fa fa-info-circle"></i> Symbol infos</label>
1345
+ <select id="node-input-symbolInfos">
1346
+ <option value="none">None</option>
1347
+ <option value="object">Object</option>
1348
+ <option value="array">Array</option>
1349
+ </select>
1350
+ </div>
1351
+ <div class="form-row">
1352
+ <label for="node-input-maxSymbols"><i class="fa fa-list-ol"></i> Output limit</label>
1353
+ <input type="number" id="node-input-maxSymbols" placeholder="100000" min="1">
1354
+ </div>
1355
+ </script>
1356
+
1357
+ <script type="text/html" data-help-name="s7-plus explore">
1358
+ <p>Browses the PLC symbol tree via the configured <b>S7+ Endpoint</b> and returns a
1359
+ <b>flat list</b> of symbol names in <code>msg.payload</code>. Use it to discover
1360
+ symbols for the <b>S7+ Read</b> / <b>S7+ Write</b> nodes or to export a symbol list.
1361
+ The node outputs names and optional metadata only — it does <b>not</b> read live values.</p>
1362
+ <p>Which DBs and memory areas are browsed (<b>Explore scope</b>) and how many symbols
1363
+ are returned (<b>Output limit</b>) are configured on the node; see <b>Details</b> and
1364
+ <b>Inputs</b> for overrides. Only leaf symbols with datatypes supported for symbolic
1365
+ access are included — the same kind you can pick with <b>Pick from PLC</b>. Any incoming
1366
+ message triggers one browse.</p>
1367
+
1368
+ <h3>Inputs</h3>
1369
+ <p>Any incoming message triggers a symbol browse using the settings on the node
1370
+ (endpoint, explore scope, output limit, symbol infos). You do not need to set
1371
+ <code>msg.payload</code> — an inject with an empty payload is enough:</p>
1372
+ <pre>[inject] → [S7+ Explore]</pre>
1373
+ <p>The optional properties below override the node configuration for <b>this message
1374
+ only</b>. They can be combined. Unset properties keep the values from the editor.</p>
1375
+ <dl class="message-properties">
1376
+ <dt class="optional">maxSymbols <span class="property-type">number</span></dt>
1377
+ <dd>Cap how many symbols are returned for this browse. Useful on large PLC programs
1378
+ when you only need a sample, or to temporarily raise the limit beyond the value
1379
+ set under <b>Output limit</b>.
1380
+ Example: <code>msg.maxSymbols = 500;</code></dd>
1381
+
1382
+ <dt class="optional">exploreScope <span class="property-type">object</span></dt>
1383
+ <dd>Limit which memory areas are browsed for this message. Same structure as the
1384
+ <b>Explore scope</b> dialog on the node:
1385
+ <ul>
1386
+ <li><code>everything: true</code> — full symbol table (default when omitted, or
1387
+ when both <code>dbs</code> and <code>areas</code> are empty).</li>
1388
+ <li><code>everything: false</code> with <code>dbs</code> and/or <code>areas</code>
1389
+ — only the listed data blocks and/or memory areas.</li>
1390
+ <li><code>dbs</code> — array of DB names, e.g.
1391
+ <code>["DB1", "RecipeData"]</code>.</li>
1392
+ <li><code>areas</code> — array of area names:
1393
+ <code>IArea</code>, <code>QArea</code>, <code>MArea</code>,
1394
+ <code>S7Timers</code>, <code>S7Counters</code>.</li>
1395
+ </ul>
1396
+ Example — symbols in DB1 and the output area only:
1397
+ <pre>msg.exploreScope = {
1398
+ everything: false,
1399
+ dbs: ["DB1"],
1400
+ areas: ["QArea"]
1401
+ };</pre></dd>
1402
+
1403
+ <dt class="optional">symbolInfos <span class="property-type">string</span></dt>
1404
+ <dd>Control whether metadata is included in <code>msg.infos</code> for this
1405
+ message. Overrides the <b>Symbol infos</b> dropdown. Valid values:
1406
+ <ul>
1407
+ <li><code>"none"</code> — symbol names only in <code>msg.payload</code>
1408
+ (smallest output).</li>
1409
+ <li><code>"object"</code> — <code>msg.infos</code> keyed by symbol name.</li>
1410
+ <li><code>"array"</code> — <code>msg.infos</code> in browse order, each entry
1411
+ includes a <code>symbol</code> field.</li>
1412
+ </ul>
1413
+ Example: <code>msg.symbolInfos = "object";</code></dd>
1414
+ </dl>
1415
+ <p>Combine overrides in a <b>function</b> node, e.g. browse one DB with metadata
1416
+ and a custom limit:</p>
1417
+ <pre>msg.exploreScope = { everything: false, dbs: ["MotorData"], areas: [] };
1418
+ msg.symbolInfos = "array";
1419
+ msg.maxSymbols = 1000;
1420
+ return msg;</pre>
1421
+
1422
+ <h3>Outputs</h3>
1423
+ <p>The <b>Symbol infos</b> option controls whether additional metadata is included
1424
+ in <code>msg.infos</code>:</p>
1425
+ <ul>
1426
+ <li><b>None</b> (default) — only symbol names in <code>msg.payload</code>.</li>
1427
+ <li><b>Object</b> — <code>msg.infos</code> is an object keyed by symbol name.</li>
1428
+ <li><b>Array</b> — <code>msg.infos</code> is an array in the same order as
1429
+ <code>msg.payload</code>, each entry includes a <code>symbol</code> field.</li>
1430
+ </ul>
1431
+ <dl class="message-properties">
1432
+ <dt>payload <span class="property-type">string[]</span></dt>
1433
+ <dd>Flat list of exported symbol names (one string per symbol in browse order).
1434
+ Works directly with Split and filter nodes. To read these symbols with
1435
+ <b>S7+ Read</b>, set <code>msg.addSymbols = msg.payload</code> in a
1436
+ <b>function</b> or <b>change</b> node before the read node (merge with
1437
+ configured symbols). For a full per-message override on Read, set
1438
+ <code>msg.symbols</code> instead.</dd>
1439
+
1440
+ <dt class="optional">infos <span class="property-type">object | array</span></dt>
1441
+ <dd>Additional metadata per symbol (datatype, access address, offsets, CRC).
1442
+ Omitted when <b>Symbol infos</b> is set to <b>None</b>.</dd>
1443
+
1444
+ <dt>meta <span class="property-type">object</span></dt>
1445
+ <dd>Summary with <code>dbCount</code>, <code>symbolCount</code>,
1446
+ <code>durationMs</code>, <code>scope</code>, and when the output limit was hit:
1447
+ <code>limitExceeded</code>, <code>maxSymbols</code>, <code>limitMessage</code>.</dd>
1448
+ </dl>
1449
+
1450
+ <h3>Details</h3>
1451
+ <p>This node does <b>not</b> read live values; it only lists the symbols. For reading or
1452
+ writing values use the <b>S7+ Read</b> / <b>S7+ Write</b> nodes. For picking individual
1453
+ symbols interactively, use the <b>Pick from PLC</b> button on those nodes.</p>
1454
+ <p>A browse can take a moment on large PLC programs. While one browse is running, further
1455
+ triggers on the same node are rejected.</p>
1456
+ <p><b>Output limit</b> (default 100000) caps how many symbols are exported on a full browse.
1457
+ When the PLC has more symbols, the first <code>maxSymbols</code> entries are still output
1458
+ normally; the node status turns <b>yellow</b> and <code>msg.meta.limitExceeded</code>
1459
+ is set to <code>true</code>. Override per message with <code>msg.maxSymbols</code>.</p>
1460
+ <p><b>Explore scope</b> limits which memory areas are browsed. Use the
1461
+ <b>Explore scope</b> button to select <b>Everything</b> (default), individual data blocks,
1462
+ and/or areas (I, Q, M, timers, counters). When a partial scope is saved with nothing
1463
+ selected, the node falls back to <b>Everything</b>. Override per message with
1464
+ <code>msg.exploreScope</code>.</p>
1465
+
1466
+ <h3>Example</h3>
1467
+ <p>Browse symbols on demand and inspect the result:</p>
1468
+ <pre>[inject] → [S7+ Explore] → [debug msg.payload]</pre>
1469
+ <p>Excerpt after browse (<b>Symbol infos</b> = Object):</p>
1470
+ <pre>msg.payload = [
1471
+ "Motor.speed",
1472
+ "Tank.level"
1473
+ ]
1474
+
1475
+ msg.infos = {
1476
+ "Motor.speed": {
1477
+ "accessSequence": "8A0E0001.A",
1478
+ "softdatatypeName": "Int",
1479
+ "softdatatype": 6,
1480
+ "optAddress": 0,
1481
+ "nonOptAddress": 0,
1482
+ "optBitoffset": 0,
1483
+ "nonOptBitoffset": 0
1484
+ }
1485
+ }
1486
+
1487
+ msg.meta = {
1488
+ "dbCount": 12,
1489
+ "symbolCount": 100000,
1490
+ "durationMs": 820,
1491
+ "limitExceeded": true,
1492
+ "maxSymbols": 100000,
1493
+ "limitMessage": "Symbol limit exceeded (100000 exported, max 100000)"
1494
+ }</pre>
1495
+ </script>
1496
+
1497
+ <script type="text/javascript">
1498
+ RED.nodes.registerType('s7-plus explore', {
1499
+ category: 'S7 Plus',
1500
+ color: '#3FADB5',
1501
+ paletteLabel: 'S7+ Explore',
1502
+ defaults: {
1503
+ name: { value: '' },
1504
+ endpoint: { value: '', type: 's7-plus endpoint', required: true },
1505
+ maxSymbols: { value: 100000, validate: RED.validators.number() },
1506
+ exploreScope: { value: { everything: true, dbs: [], areas: [] } },
1507
+ symbolInfos: { value: 'none' }
1508
+ },
1509
+ inputs: 1,
1510
+ outputs: 1,
1511
+ icon: 's7complus.svg',
1512
+ label: function () {
1513
+ return this.name || 'S7+ Explore';
1514
+ },
1515
+ labelStyle: function () {
1516
+ return this.name ? 'node_label_italic' : '';
1517
+ },
1518
+ oneditprepare: function () {
1519
+ $('#node-input-symbolInfos').val(this.symbolInfos || 'none');
1520
+ const scope = window.s7complusNormalizeExploreScope(this.exploreScope);
1521
+ window.s7complusExploreScopeDraft = scope;
1522
+ $('#s7complus-explore-scope-summary').text(window.s7complusExploreScopeSummary(scope));
1523
+ $('#s7complus-explore-scope-btn').on('click', function () {
1524
+ const endpointId = $('#node-input-endpoint').val();
1525
+ window.s7complusOpenExploreScopeDialog({
1526
+ context: window.s7complusEndpointBrowseContext(endpointId),
1527
+ scope: window.s7complusExploreScopeDraft,
1528
+ onSaved: function (saved) {
1529
+ window.s7complusExploreScopeDraft = saved;
1530
+ $('#s7complus-explore-scope-summary')
1531
+ .text(window.s7complusExploreScopeSummary(saved));
1532
+ }
1533
+ });
1534
+ });
1535
+ },
1536
+ oneditsave: function () {
1537
+ this.symbolInfos = $('#node-input-symbolInfos').val() || 'none';
1538
+ this.exploreScope = window.s7complusNormalizeExploreScope(window.s7complusExploreScopeDraft);
1539
+ }
1540
+ });
1541
+ </script>