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,91 @@
1
+ 'use strict';
2
+
3
+ const DEFAULT_MAX_SYMBOLS = 100000;
4
+ const { normalizeExploreScope } = require('../lib/s7plus/browse/areas');
5
+ const { formatExplorePayload, normalizeSymbolInfos } = require('../lib/s7plus/explore-result');
6
+
7
+ function parseLimit(value, fallback = DEFAULT_MAX_SYMBOLS) {
8
+ if (value === undefined || value === null || value === '') return fallback;
9
+ const n = parseInt(value, 10);
10
+ if (!Number.isFinite(n) || n <= 0) return fallback;
11
+ return n;
12
+ }
13
+
14
+ module.exports = function (RED) {
15
+ function S7ComPlusExplore(config) {
16
+ RED.nodes.createNode(this, config);
17
+ const node = this;
18
+ node.endpoint = RED.nodes.getNode(config.endpoint);
19
+ if (!node.endpoint) {
20
+ node.error('Missing s7-plus endpoint configuration');
21
+ return;
22
+ }
23
+
24
+ const configuredMaxSymbols = parseLimit(config.maxSymbols, DEFAULT_MAX_SYMBOLS);
25
+ const configuredScope = normalizeExploreScope(config.exploreScope);
26
+ const configuredSymbolInfos = normalizeSymbolInfos(config.symbolInfos);
27
+
28
+ let busy = false;
29
+
30
+ node.on('input', async (msg, send, done) => {
31
+ send = send || function () { node.send.apply(node, arguments); };
32
+ done = done || function (err) { if (err) node.error(err, msg); };
33
+
34
+ if (busy) {
35
+ node.warn('Browse already in progress');
36
+ node.status({ fill: 'yellow', shape: 'ring', text: 'skipped (busy)' });
37
+ done();
38
+ return;
39
+ }
40
+ busy = true;
41
+ node.status({ fill: 'blue', shape: 'ring', text: 'browsing...' });
42
+ const t0 = Date.now();
43
+ const maxSymbols = parseLimit(
44
+ msg && msg.maxSymbols !== undefined ? msg.maxSymbols : configuredMaxSymbols,
45
+ configuredMaxSymbols
46
+ );
47
+ const scope = normalizeExploreScope(
48
+ msg && msg.exploreScope !== undefined ? msg.exploreScope : configuredScope
49
+ );
50
+ const symbolInfos = normalizeSymbolInfos(
51
+ msg && msg.symbolInfos !== undefined ? msg.symbolInfos : configuredSymbolInfos
52
+ );
53
+
54
+ try {
55
+ const result = await node.endpoint.browseFull({ maxSymbols, scope });
56
+ const elapsed = Date.now() - t0;
57
+ const formatted = formatExplorePayload(result.symbols, symbolInfos);
58
+ msg.payload = formatted.symbols;
59
+ if (formatted.infos !== undefined) {
60
+ msg.infos = formatted.infos;
61
+ } else {
62
+ delete msg.infos;
63
+ }
64
+ msg.meta = result.meta;
65
+ if (result.meta.limitExceeded) {
66
+ node.status({
67
+ fill: 'yellow',
68
+ shape: 'dot',
69
+ text: `${result.meta.symbolCount}/${result.meta.maxSymbols} symbols (limit)`
70
+ });
71
+ } else {
72
+ node.status({
73
+ fill: 'green',
74
+ shape: 'dot',
75
+ text: `${result.meta.symbolCount} symbol(s) (${elapsed}ms)`
76
+ });
77
+ }
78
+ send(msg);
79
+ done();
80
+ } catch (e) {
81
+ const elapsed = Date.now() - t0;
82
+ node.status({ fill: 'red', shape: 'dot', text: `${e.message} (${elapsed}ms)` });
83
+ done(e);
84
+ } finally {
85
+ busy = false;
86
+ }
87
+ });
88
+ }
89
+
90
+ RED.nodes.registerType('s7-plus explore', S7ComPlusExplore);
91
+ };
@@ -0,0 +1,255 @@
1
+ 'use strict';
2
+
3
+ const { isHexAddress, isSymbolicName } = require('../lib/s7plus/tag-routing');
4
+ const { formatOutputPayload } = require('../lib/s7plus/read-result');
5
+
6
+ /**
7
+ * Normalize a tag entry from config or msg.symbols into a consistent shape.
8
+ * Routing priority:
9
+ * 1. Pre-computed symbolCrc + hex address → readTags with CRC (already secured)
10
+ * 2. Symbolic address (no hex) → resolveAndRead
11
+ * 3. Hex address + symbolic name → resolveAndRead (CRC protection for browsed vars)
12
+ * 4. Hex address only → readTags without CRC (legacy)
13
+ */
14
+ function normalizeTag(t, i) {
15
+ if (!t) throw new Error(`Symbol #${i} is empty`);
16
+ if (typeof t === 'string') {
17
+ return { name: t, address: t, symbolic: !isHexAddress(t) };
18
+ }
19
+ if (typeof t === 'object') {
20
+ if (t.address) {
21
+ const addrIsHex = isHexAddress(t.address);
22
+
23
+ if (addrIsHex && t.symbolCrc) {
24
+ return {
25
+ name: t.name || t.address,
26
+ address: t.address,
27
+ datatype: t.datatype,
28
+ symbolCrc: t.symbolCrc,
29
+ symbolic: false
30
+ };
31
+ }
32
+
33
+ if (!addrIsHex) {
34
+ return {
35
+ name: t.name || t.address,
36
+ address: t.address,
37
+ datatype: t.datatype,
38
+ symbolic: true
39
+ };
40
+ }
41
+
42
+ if (isSymbolicName(t.name)) {
43
+ return {
44
+ name: t.name,
45
+ address: t.name,
46
+ datatype: t.datatype,
47
+ symbolic: true
48
+ };
49
+ }
50
+
51
+ return {
52
+ name: t.name || t.address,
53
+ address: t.address,
54
+ datatype: t.datatype,
55
+ symbolic: false
56
+ };
57
+ }
58
+ if (t.symbol) {
59
+ return { name: t.name || t.symbol, address: t.symbol, symbolic: true };
60
+ }
61
+ }
62
+ throw new Error(`Symbol #${i} has no address or symbol`);
63
+ }
64
+
65
+ function parseAddSymbols(msg) {
66
+ if (!Array.isArray(msg.addSymbols) || msg.addSymbols.length === 0) {
67
+ return [];
68
+ }
69
+ if (!msg.addSymbols.every(s => typeof s === 'string')) {
70
+ return [];
71
+ }
72
+ return msg.addSymbols;
73
+ }
74
+
75
+ /**
76
+ * Parse a per-message symbol override from msg.symbols.
77
+ * Only a (non-empty) array of strings is accepted; anything else is rejected.
78
+ * Returns undefined when msg.symbols is not set (use configured symbols),
79
+ * or an empty array when it is an empty array (also falls back to configured).
80
+ */
81
+ function parseMsgSymbols(msg) {
82
+ if (msg.symbols === undefined || msg.symbols === null) {
83
+ return undefined;
84
+ }
85
+ if (!Array.isArray(msg.symbols)) {
86
+ throw new Error('msg.symbols must be an array of strings');
87
+ }
88
+ if (!msg.symbols.every(s => typeof s === 'string')) {
89
+ throw new Error('msg.symbols must be an array of strings');
90
+ }
91
+ return msg.symbols;
92
+ }
93
+
94
+ module.exports = function (RED) {
95
+ function S7ComPlusIn(config) {
96
+ RED.nodes.createNode(this, config);
97
+ const node = this;
98
+ node.endpoint = RED.nodes.getNode(config.endpoint);
99
+ if (!node.endpoint) {
100
+ node.error('Missing s7-plus endpoint configuration');
101
+ return;
102
+ }
103
+
104
+ const configuredSymbols = Array.isArray(config.symbols) ? config.symbols : [];
105
+
106
+ let busy = false;
107
+ let lastPayloadString = ""; // 用於 Diff 判斷
108
+ let cycleTimer = null; // 用於自動讀取
109
+
110
+ // 將讀取核心邏輯獨立成一個函數
111
+ const executeRead = async (msg) => {
112
+ // 兼容外部 Inject 或是自動輪詢的 send/done 處理
113
+ const sendFn = msg._send || function () { node.send.apply(node, arguments); };
114
+ const doneFn = msg._done || function (err) { if (err) node.error(err, msg); };
115
+
116
+ if (busy) {
117
+ node.status({ fill: 'grey', shape: 'ring', text: 'skipped (busy)' });
118
+ if (msg._done) doneFn(); // 只有外部手動觸發才回報完成
119
+ return;
120
+ }
121
+ busy = true;
122
+
123
+ try {
124
+ const addPaths = parseAddSymbols(msg);
125
+ let msgSymbols;
126
+ try {
127
+ msgSymbols = parseMsgSymbols(msg);
128
+ } catch (e) {
129
+ doneFn(e);
130
+ return;
131
+ }
132
+
133
+ const source = msgSymbols && msgSymbols.length ? msgSymbols : configuredSymbols;
134
+ let tags = [];
135
+ try {
136
+ if (source.length) tags = source.map(normalizeTag);
137
+ } catch (e) {
138
+ doneFn(e);
139
+ return;
140
+ }
141
+
142
+ const configuredSymbolicPaths = tags.filter(t => t.symbolic).map(t => t.address);
143
+ const hexTags = tags.filter(t => !t.symbolic);
144
+ const allSymbolicPaths = [...new Set([...configuredSymbolicPaths, ...addPaths])];
145
+
146
+ if (allSymbolicPaths.length === 0 && hexTags.length === 0) {
147
+ doneFn(new Error('No symbols configured'));
148
+ return;
149
+ }
150
+
151
+ node.status({ fill: 'blue', shape: 'ring', text: 'reading' });
152
+ const t0 = Date.now();
153
+
154
+ let result = {};
155
+
156
+ // 執行通訊讀取
157
+ if (allSymbolicPaths.length > 0) {
158
+ const symbolicResult = await node.endpoint.resolveAndRead(allSymbolicPaths);
159
+ Object.assign(result, symbolicResult);
160
+ }
161
+ if (hexTags.length > 0) {
162
+ const hexResult = await node.endpoint.readTags(hexTags);
163
+ Object.assign(result, hexResult);
164
+ }
165
+
166
+ const elapsed = Date.now() - t0;
167
+ // 注意:這裡是用 t.name (也就是你的自訂義 Alias) 來當作輸出的 Key
168
+ const outputOrder = [...allSymbolicPaths, ...hexTags.map((t) => t.name)];
169
+
170
+ // --- 修改:支援單一數值直接輸出 ---
171
+ if (config.outputFormat === 'value') {
172
+ if (outputOrder.length === 1) {
173
+ const singleKey = outputOrder[0];
174
+ // 直接將數值剝離出來賦予 payload
175
+ msg.payload = result[singleKey] ? result[singleKey].value : null;
176
+ } else {
177
+ // 防呆機制:如果有人用外部 inject msg.symbols 強制讀多個點位,自動降級成 object
178
+ node.warn("Output set to 'Value' but multiple tags requested. Falling back to 'Object' format.");
179
+ msg.payload = formatOutputPayload(result, outputOrder, 'object');
180
+ }
181
+ } else {
182
+ // 原本的 Object 或 Array 輸出模式
183
+ msg.payload = formatOutputPayload(result, outputOrder, config.outputFormat);
184
+ }
185
+ // ---------------------------------
186
+
187
+ // --- 新增:Diff Filter 邏輯 ---
188
+ if (config.diff) {
189
+ const currentPayloadString = JSON.stringify(msg.payload);
190
+ if (currentPayloadString === lastPayloadString) {
191
+ // 數值沒變,直接結束,不輸出 msg
192
+ busy = false;
193
+ if (msg._done) doneFn();
194
+ return;
195
+ }
196
+ lastPayloadString = currentPayloadString;
197
+ }
198
+ // -----------------------------
199
+
200
+ const allTags = Object.values(result);
201
+ const failed = allTags.filter(t => t.status !== 'ok').length;
202
+ if (failed > 0) {
203
+ node.status({
204
+ fill: 'yellow',
205
+ shape: 'dot',
206
+ text: `read ${allTags.length - failed}/${allTags.length} ok (${elapsed}ms)`
207
+ });
208
+ } else {
209
+ node.status({ fill: 'green', shape: 'dot', text: `read ${allTags.length} (${elapsed}ms)` });
210
+ }
211
+
212
+ sendFn(msg);
213
+ doneFn();
214
+
215
+ } catch (e) {
216
+ const elapsed = Date.now() - Date.now(); // 錯誤時的簡化處理
217
+ node.status({ fill: 'red', shape: 'dot', text: `${e.message}` });
218
+ doneFn(e);
219
+ } finally {
220
+ busy = false;
221
+ }
222
+ };
223
+
224
+ // 監聽外部 Inject 節點 (手動觸發)
225
+ node.on('input', (msg, send, done) => {
226
+ msg._send = send;
227
+ msg._done = done;
228
+ executeRead(msg);
229
+ });
230
+
231
+ // --- 新增:啟動自動讀取定時器 ---
232
+ if (config.readMode === 'auto') {
233
+ let cycleMs = parseInt(config.readCycle, 10) || 1000;
234
+ if (cycleMs < 100) cycleMs = 100; // 最低限制 100ms 保護機制
235
+ cycleTimer = setInterval(() => {
236
+ // 自動觸發不需要傳入 _send 與 _done,因為不用回應前面的 inject
237
+ executeRead({ payload: '' });
238
+ }, cycleMs);
239
+ }
240
+
241
+ // 節點關閉或重新部署時,清除定時器
242
+ node.on('close', (done) => {
243
+ if (cycleTimer) {
244
+ clearInterval(cycleTimer);
245
+ cycleTimer = null;
246
+ }
247
+ done();
248
+ });
249
+ }
250
+
251
+ RED.nodes.registerType('s7-plus read', S7ComPlusIn);
252
+ };
253
+
254
+ module.exports.parseAddSymbols = parseAddSymbols;
255
+ module.exports.parseMsgSymbols = parseMsgSymbols;
@@ -0,0 +1,182 @@
1
+ 'use strict';
2
+
3
+ const { isHexAddress, isSymbolicName } = require('../lib/s7plus/tag-routing');
4
+ const { formatOutputPayload } = require('../lib/s7plus/read-result');
5
+
6
+ /**
7
+ * Normalize a write symbol from config or msg.symbols and classify it
8
+ * as symbolic vs. hex, mirroring the read node's normalizeTag routing:
9
+ * 1. hex address + symbolCrc -> CRC-secured writeTags
10
+ * 2. symbolic address (no hex) -> resolveAndWrite
11
+ * 3. hex address + symbolic name -> resolveAndWrite (CRC protection)
12
+ * 4. hex address only -> writeTags (legacy, no CRC)
13
+ */
14
+ function normalizeSymbol(v, i) {
15
+ if (!v) throw new Error(`Symbol #${i} is empty`);
16
+ if (typeof v === 'string') {
17
+ return { name: v, address: v, datatype: undefined, symbolic: !isHexAddress(v) };
18
+ }
19
+
20
+ const address = v.address || v.symbol;
21
+ if (!address) throw new Error(`Symbol #${i} has no address`);
22
+ const addrIsHex = isHexAddress(address);
23
+
24
+ if (addrIsHex && v.symbolCrc) {
25
+ return {
26
+ name: v.name || address,
27
+ address,
28
+ datatype: v.datatype,
29
+ symbolCrc: v.symbolCrc,
30
+ symbolic: false
31
+ };
32
+ }
33
+ if (!addrIsHex) {
34
+ return { name: v.name || address, address, datatype: v.datatype, symbolic: true };
35
+ }
36
+ if (isSymbolicName(v.name)) {
37
+ return { name: v.name, address: v.name, datatype: v.datatype, symbolic: true };
38
+ }
39
+ return { name: v.name || address, address, datatype: v.datatype, symbolic: false };
40
+ }
41
+
42
+ function resolveValueForSymbol(payload, symbol, isOnlySymbol) {
43
+ if (payload && typeof payload === 'object' && !Buffer.isBuffer(payload) && symbol.name in payload) {
44
+ return payload[symbol.name];
45
+ }
46
+ if (isOnlySymbol) return payload;
47
+ throw new Error(`msg.payload is missing value for "${symbol.name}"`);
48
+ }
49
+
50
+ function formatStatusValue(value) {
51
+ if (typeof value === 'boolean') return String(value);
52
+ if (value == null) return 'null';
53
+ if (Buffer.isBuffer(value)) return value.toString('hex');
54
+ if (typeof value === 'object') {
55
+ const s = JSON.stringify(value);
56
+ return s.length > 40 ? `${s.slice(0, 37)}...` : s;
57
+ }
58
+ const s = String(value);
59
+ return s.length > 40 ? `${s.slice(0, 37)}...` : s;
60
+ }
61
+
62
+ function writeStatusText(tags) {
63
+ if (tags.length === 1) {
64
+ return `wrote ${formatStatusValue(tags[0].value)}`;
65
+ }
66
+ return `wrote ${tags.length} tags`;
67
+ }
68
+
69
+ module.exports = function (RED) {
70
+ function S7ComPlusOut(config) {
71
+ RED.nodes.createNode(this, config);
72
+ const node = this;
73
+ node.endpoint = RED.nodes.getNode(config.endpoint);
74
+ if (!node.endpoint) {
75
+ node.error('Missing s7-plus endpoint configuration');
76
+ return;
77
+ }
78
+
79
+ const configuredSymbols = Array.isArray(config.symbols) ? config.symbols : [];
80
+
81
+ // Same backpressure as s7-plus read: silently drop input
82
+ // messages while a write is still in flight, so a fast inject
83
+ // cannot pile up writes that overflow Node-RED's queue and the
84
+ // endpoint's user-lock chain.
85
+ let busy = false;
86
+
87
+ node.on('input', async (msg, send, done) => {
88
+ send = send || function () { node.send.apply(node, arguments); };
89
+ done = done || function (err) { if (err) node.error(err, msg); };
90
+
91
+ if (busy) {
92
+ node.status({ fill: 'grey', shape: 'ring', text: 'skipped (busy)' });
93
+ done();
94
+ return;
95
+ }
96
+ busy = true;
97
+
98
+ try {
99
+ const source = Array.isArray(msg.symbols) && msg.symbols.length
100
+ ? msg.symbols
101
+ : configuredSymbols;
102
+
103
+ if (!source.length) {
104
+ done(new Error('No symbols configured'));
105
+ return;
106
+ }
107
+
108
+ let symbols;
109
+ try {
110
+ symbols = source.map(normalizeSymbol);
111
+ } catch (e) {
112
+ done(e);
113
+ return;
114
+ }
115
+
116
+ const isSingle = symbols.length === 1;
117
+ let tags;
118
+ try {
119
+ tags = symbols.map((v) => ({
120
+ name: v.name,
121
+ address: v.address,
122
+ symbolCrc: v.symbolCrc,
123
+ symbolic: v.symbolic,
124
+ datatype: v.datatype || (msg.datatype && isSingle ? msg.datatype : undefined),
125
+ value: resolveValueForSymbol(msg.payload, v, isSingle)
126
+ }));
127
+ } catch (e) {
128
+ node.status({ fill: 'red', shape: 'dot', text: e.message });
129
+ done(e);
130
+ return;
131
+ }
132
+
133
+ // Symbolic tags must be resolved to a hex address + CRC
134
+ // before writing (resolveAndWrite); hex tags go straight
135
+ // to the CRC-aware writeTags. Same split as the read node.
136
+ const symbolicTags = tags.filter(t => t.symbolic);
137
+ const hexTags = tags.filter(t => !t.symbolic);
138
+
139
+ const t0 = Date.now();
140
+ try {
141
+ // Merge per-tag results from both write paths into a
142
+ // single object keyed by name, mirroring the read node.
143
+ const result = {};
144
+ if (symbolicTags.length > 0) {
145
+ Object.assign(result, await node.endpoint.resolveAndWrite(symbolicTags));
146
+ }
147
+ if (hexTags.length > 0) {
148
+ Object.assign(result, await node.endpoint.writeTags(hexTags));
149
+ }
150
+
151
+ const elapsed = Date.now() - t0;
152
+ const outputOrder = tags.map((t) => t.name);
153
+ msg.payload = formatOutputPayload(result, outputOrder, config.outputFormat);
154
+ const allTags = Object.values(result);
155
+ const failed = allTags.filter(t => t.status !== 'ok').length;
156
+ if (failed > 0) {
157
+ node.status({
158
+ fill: 'yellow',
159
+ shape: 'dot',
160
+ text: `wrote ${allTags.length - failed}/${allTags.length} ok (${elapsed}ms)`
161
+ });
162
+ } else {
163
+ node.status({
164
+ fill: 'green',
165
+ shape: 'dot',
166
+ text: `${writeStatusText(tags)} (${elapsed}ms)`
167
+ });
168
+ }
169
+ send(msg);
170
+ done();
171
+ } catch (e) {
172
+ node.status({ fill: 'red', shape: 'dot', text: e.message });
173
+ done(e);
174
+ }
175
+ } finally {
176
+ busy = false;
177
+ }
178
+ });
179
+ }
180
+
181
+ RED.nodes.registerType('s7-plus write', S7ComPlusOut);
182
+ };