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.
- package/README.txt +69 -0
- package/node-red-contrib-mysql-extended-test/mysql-extended.html +443 -0
- package/node-red-contrib-mysql-extended-test/mysql-extended.js +355 -0
- package/node-red-contrib-rfid-helper/rfid-lazy.html +153 -0
- package/node-red-contrib-rfid-helper/rfid-lazy.js +361 -0
- package/node-red-contrib-s7-plus/LICENSE +235 -0
- package/node-red-contrib-s7-plus/README.md +141 -0
- package/node-red-contrib-s7-plus/examples/read-multiple-values-flow.json +346 -0
- package/node-red-contrib-s7-plus/examples/read-write-test-flow.json +172 -0
- package/node-red-contrib-s7-plus/examples/write-single-values-flow.json +5583 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/README.md +72 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/areas.js +36 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/datatypes.js +91 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/flat-browser.js +433 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/index.js +22 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/lazy.js +397 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/node-id.js +22 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/resolve-symbolic.js +181 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/vte-helpers.js +49 -0
- package/node-red-contrib-s7-plus/lib/s7plus/buffer-stream.js +96 -0
- package/node-red-contrib-s7-plus/lib/s7plus/client.js +1238 -0
- package/node-red-contrib-s7-plus/lib/s7plus/constants.js +155 -0
- package/node-red-contrib-s7-plus/lib/s7plus/crc/index.js +3 -0
- package/node-red-contrib-s7-plus/lib/s7plus/crc/s7-crc32.js +70 -0
- package/node-red-contrib-s7-plus/lib/s7plus/crc/symbol-crc.js +271 -0
- package/node-red-contrib-s7-plus/lib/s7plus/debug.js +88 -0
- package/node-red-contrib-s7-plus/lib/s7plus/explore-result.js +41 -0
- package/node-red-contrib-s7-plus/lib/s7plus/item-address.js +55 -0
- package/node-red-contrib-s7-plus/lib/s7plus/pdu-explore.js +77 -0
- package/node-red-contrib-s7-plus/lib/s7plus/pdu-messages.js +359 -0
- package/node-red-contrib-s7-plus/lib/s7plus/pobject.js +197 -0
- package/node-red-contrib-s7-plus/lib/s7plus/pvalue-codec.js +435 -0
- package/node-red-contrib-s7-plus/lib/s7plus/pvalue.js +653 -0
- package/node-red-contrib-s7-plus/lib/s7plus/pvar-lists.js +336 -0
- package/node-red-contrib-s7-plus/lib/s7plus/read-result.js +91 -0
- package/node-red-contrib-s7-plus/lib/s7plus/s7p.js +266 -0
- package/node-red-contrib-s7-plus/lib/s7plus/tag-routing.js +30 -0
- package/node-red-contrib-s7-plus/lib/s7plus/transport/cotp-stream.js +335 -0
- package/node-red-contrib-s7-plus/lib/s7plus/transport/s7-transport.js +335 -0
- package/node-red-contrib-s7-plus/nodes/icons/s7complus.png +0 -0
- package/node-red-contrib-s7-plus/nodes/icons/s7complus.svg +17 -0
- package/node-red-contrib-s7-plus/nodes/s7complus-endpoint.js +825 -0
- package/node-red-contrib-s7-plus/nodes/s7complus-explore.js +91 -0
- package/node-red-contrib-s7-plus/nodes/s7complus-in.js +255 -0
- package/node-red-contrib-s7-plus/nodes/s7complus-out.js +182 -0
- package/node-red-contrib-s7-plus/nodes/s7complus.html +1541 -0
- package/node-red-contrib-s7-plus/nodes/s7complus.js +10 -0
- package/node-red-contrib-s7-plus/package.json +50 -0
- package/node-red-contrib-s7-plus/plc/s7-1500/DB_Arrays.db +17 -0
- package/node-red-contrib-s7-plus/plc/s7-1500/DB_Binary.db +16 -0
- package/node-red-contrib-s7-plus/plc/s7-1500/DB_BitStrings.db +36 -0
- package/node-red-contrib-s7-plus/plc/s7-1500/DB_CharacterStrings.db +42 -0
- package/node-red-contrib-s7-plus/plc/s7-1500/DB_DateAndTime.db +70 -0
- package/node-red-contrib-s7-plus/plc/s7-1500/DB_FloatingPoint.db +42 -0
- package/node-red-contrib-s7-plus/plc/s7-1500/DB_Integers.db +72 -0
- package/node-red-contrib-s7-plus/plc/s7-1500/DB_Timers.db +40 -0
- package/node-red-contrib-s7-plus/scripts/example-flow-shared.js +39 -0
- package/node-red-contrib-s7-plus/scripts/generate-read-multiple-flow.js +331 -0
- package/node-red-contrib-s7-plus/scripts/generate-rw-test-flow.js +690 -0
- package/node-red-contrib-s7-plus/scripts/generate-write-flow.js +476 -0
- package/node-red-contrib-s7-plus/scripts/layout-write-flow.js +156 -0
- package/node-red-contrib-s7-plus/scripts/run-tests.js +16 -0
- package/node-red-flowgobal-change/variable-change.html +425 -0
- package/node-red-flowgobal-change/variable-change.js +92 -0
- package/node-red-ui_media_viewer/ui_media_viewer.html +87 -0
- package/node-red-ui_media_viewer/ui_media_viewer.js +136 -0
- 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> </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 => 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 — 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 — 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 => 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 — 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>
|