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,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
|
+
};
|