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,690 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Generates examples/read-write-test-flow.json: an automated read/write verification
|
|
5
|
+
// flow. A single inject starts a sequential test that, for every constant of
|
|
6
|
+
// every scalar datatype defined in plc/s7-1500, writes the value into the
|
|
7
|
+
// matching *_write tag, reads it back and compares write vs. read-back.
|
|
8
|
+
//
|
|
9
|
+
// The test never aborts on a failure: it runs to completion and emits one
|
|
10
|
+
// result per datatype plus a final summary. A central "Test Runner" function
|
|
11
|
+
// node orchestrates one generic s7-plus write and one s7-plus read node
|
|
12
|
+
// strictly sequentially (both nodes drop messages while busy).
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { EP, DEFAULT_OUTPUT_FORMAT, endpointNode, globalConfigNode } = require('./example-flow-shared');
|
|
18
|
+
|
|
19
|
+
const TAB = 'rw_test_tab';
|
|
20
|
+
|
|
21
|
+
// Scalar datatype sections, mirroring scripts/generate-write-flow.js. One
|
|
22
|
+
// *_write tag per datatype; the test values come from every constant of that
|
|
23
|
+
// datatype defined in the matching DB. Hardware/system datatypes are excluded.
|
|
24
|
+
const SECTIONS = [
|
|
25
|
+
{
|
|
26
|
+
title: 'Binary',
|
|
27
|
+
db: 'DB_Binary',
|
|
28
|
+
items: [{ var: 'Bool_write', dt: 'Bool' }]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
title: 'BitStrings',
|
|
32
|
+
db: 'DB_BitStrings',
|
|
33
|
+
items: [
|
|
34
|
+
{ var: 'Byte_write', dt: 'Byte' },
|
|
35
|
+
{ var: 'Word_write', dt: 'Word' },
|
|
36
|
+
{ var: 'DWord_write', dt: 'DWord' },
|
|
37
|
+
{ var: 'LWord_write', dt: 'LWord' }
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
title: 'CharacterStrings',
|
|
42
|
+
db: 'DB_CharacterStrings',
|
|
43
|
+
items: [
|
|
44
|
+
{ var: 'Char_write', dt: 'Char' },
|
|
45
|
+
{ var: 'String_write', dt: 'String' },
|
|
46
|
+
{ var: 'WChar_write', dt: 'WChar' },
|
|
47
|
+
{ var: 'WString_write', dt: 'WString' }
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
title: 'Integers',
|
|
52
|
+
db: 'DB_Integers',
|
|
53
|
+
items: [
|
|
54
|
+
{ var: 'SInt_write', dt: 'SInt' },
|
|
55
|
+
{ var: 'Int_write', dt: 'Int' },
|
|
56
|
+
{ var: 'DInt_write', dt: 'DInt' },
|
|
57
|
+
{ var: 'USInt_write', dt: 'USInt' },
|
|
58
|
+
{ var: 'UInt_write', dt: 'UInt' },
|
|
59
|
+
{ var: 'UDInt_write', dt: 'UDInt' },
|
|
60
|
+
{ var: 'LInt_write', dt: 'LInt' },
|
|
61
|
+
{ var: 'ULInt_write', dt: 'ULInt' }
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
title: 'FloatingPoint',
|
|
66
|
+
db: 'DB_FloatingPoint',
|
|
67
|
+
items: [
|
|
68
|
+
{ var: 'Real_write', dt: 'Real' },
|
|
69
|
+
{ var: 'LReal_write', dt: 'LReal' }
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
title: 'DateAndTime',
|
|
74
|
+
db: 'DB_DateAndTime',
|
|
75
|
+
items: [
|
|
76
|
+
{ var: 'Date_write', dt: 'Date' },
|
|
77
|
+
{ var: 'Time_Of_Day_write', dt: 'TimeOfDay' },
|
|
78
|
+
{ var: 'Date_And_Time_write', dt: 'DateAndTime' },
|
|
79
|
+
{ var: 'LTOD_write', dt: 'LTod' },
|
|
80
|
+
{ var: 'LDT_write', dt: 'Ldt' },
|
|
81
|
+
{ var: 'DTL_write', dt: 'Dtl' }
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
title: 'Timers',
|
|
86
|
+
db: 'DB_Timers',
|
|
87
|
+
items: [
|
|
88
|
+
{ var: 'Time_write', dt: 'Time' },
|
|
89
|
+
{ var: 'S5Time_write', dt: 'S5Time' },
|
|
90
|
+
{ var: 'LTime_write', dt: 'LTime' }
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
// Maps PLC datatype keywords (as written in the .db source) to the node datatype.
|
|
96
|
+
const PLC_TO_DT = {
|
|
97
|
+
Bool: 'Bool',
|
|
98
|
+
Byte: 'Byte', Word: 'Word', DWord: 'DWord', LWord: 'LWord',
|
|
99
|
+
Char: 'Char', String: 'String', WChar: 'WChar', WString: 'WString',
|
|
100
|
+
SInt: 'SInt', Int: 'Int', DInt: 'DInt',
|
|
101
|
+
USInt: 'USInt', UInt: 'UInt', UDInt: 'UDInt', LInt: 'LInt', ULInt: 'ULInt',
|
|
102
|
+
Real: 'Real', LReal: 'LReal',
|
|
103
|
+
Date: 'Date', Time_Of_Day: 'TimeOfDay', Date_And_Time: 'DateAndTime',
|
|
104
|
+
LTime_Of_Day: 'LTod', LDT: 'Ldt', DTL: 'Dtl',
|
|
105
|
+
Time: 'Time', S5Time: 'S5Time', LTime: 'LTime'
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
function slug(s) {
|
|
109
|
+
return s.replace(/[^a-zA-Z0-9]+/g, '_').replace(/^_|_$/g, '').toLowerCase();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Literal helpers (shared shape with scripts/generate-write-flow.js)
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
function quoted(lit) {
|
|
117
|
+
const m = lit.match(/'([^']*)'/);
|
|
118
|
+
return m ? m[1] : '';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function todMs(lit) {
|
|
122
|
+
const m = lit.match(/(\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?/);
|
|
123
|
+
const ms = m[4] ? Number((m[4] + '000').slice(0, 3)) : 0;
|
|
124
|
+
return String(((+m[1] * 3600) + (+m[2] * 60) + (+m[3])) * 1000 + ms);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function ltodNs(lit) {
|
|
128
|
+
const m = lit.match(/(\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?/);
|
|
129
|
+
const ns = m[4] ? BigInt((m[4] + '000000000').slice(0, 9)) : 0n;
|
|
130
|
+
const secs = BigInt((+m[1]) * 3600 + (+m[2]) * 60 + (+m[3]));
|
|
131
|
+
return (secs * 1000000000n + ns).toString();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function durationMs(lit) {
|
|
135
|
+
const body = lit.replace(/^[A-Za-z0-9]+#/, '');
|
|
136
|
+
const neg = body.startsWith('-');
|
|
137
|
+
const s = body.replace(/^-/, '').replace(/_/g, '');
|
|
138
|
+
const units = { D: 86400000, H: 3600000, M: 60000, S: 1000, MS: 1 };
|
|
139
|
+
let ms = 0;
|
|
140
|
+
const re = /(\d+)(MS|D|H|M|S)/g;
|
|
141
|
+
let m;
|
|
142
|
+
while ((m = re.exec(s))) ms += (+m[1]) * units[m[2]];
|
|
143
|
+
return String(neg ? -ms : ms);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function durationNs(lit) {
|
|
147
|
+
const body = lit.replace(/^[A-Za-z0-9]+#/, '');
|
|
148
|
+
const neg = body.startsWith('-');
|
|
149
|
+
const s = body.replace(/^-/, '').replace(/_/g, '');
|
|
150
|
+
const units = {
|
|
151
|
+
D: 86400000000000n, H: 3600000000000n, M: 60000000000n, S: 1000000000n,
|
|
152
|
+
MS: 1000000n, US: 1000n, NS: 1n
|
|
153
|
+
};
|
|
154
|
+
let ns = 0n;
|
|
155
|
+
const re = /(\d+)(MS|US|NS|D|H|M|S)/g;
|
|
156
|
+
let m;
|
|
157
|
+
while ((m = re.exec(s))) ns += BigInt(m[1]) * units[m[2]];
|
|
158
|
+
return (neg ? -ns : ns).toString();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Splits an S7 date-time literal into its components, with the fractional
|
|
162
|
+
// seconds truncated/padded to `frac` digits (3 = milliseconds).
|
|
163
|
+
function dateTimeParts(lit, frac) {
|
|
164
|
+
const m = lit.match(/(\d{4})-(\d{2})-(\d{2})-(\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?/);
|
|
165
|
+
const fill = '0'.repeat(frac);
|
|
166
|
+
const f = m[7] ? Number((m[7] + fill).slice(0, frac)) : 0;
|
|
167
|
+
return { y: +m[1], mo: +m[2], d: +m[3], h: +m[4], mi: +m[5], s: +m[6], frac: f };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// literalToTestCase: turn a DB constant value into a JS expression string that
|
|
172
|
+
// is used BOTH as the value written to the PLC and as the expected read-back
|
|
173
|
+
// value (the per-datatype comparator handles type/precision, e.g. float32 for
|
|
174
|
+
// Real, BigInt for 64-bit types, Date.getTime() for date/time types).
|
|
175
|
+
//
|
|
176
|
+
// We compare write vs. read-back of the *_write tag, NOT against the DB's
|
|
177
|
+
// stored constant; so even datatype default values (constants without a BEGIN
|
|
178
|
+
// assignment) round-trip self-consistently.
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
const DEFAULT_EXPR = {
|
|
182
|
+
Bool: 'false',
|
|
183
|
+
Byte: '0', Word: '0', DWord: '0', LWord: '0n',
|
|
184
|
+
Char: '" "', WChar: '" "', String: '""', WString: '""',
|
|
185
|
+
SInt: '0', Int: '0', DInt: '0', USInt: '0', UInt: '0', UDInt: '0',
|
|
186
|
+
LInt: '0n', ULInt: '0n',
|
|
187
|
+
Real: '0', LReal: '0',
|
|
188
|
+
Date: 'new Date(1990, 0, 1)',
|
|
189
|
+
DateAndTime: 'new Date(Date.UTC(1990, 0, 1, 0, 0, 0, 0))',
|
|
190
|
+
Ldt: 'new Date(0)',
|
|
191
|
+
Dtl: 'new Date(0)',
|
|
192
|
+
TimeOfDay: '0', LTod: '0n',
|
|
193
|
+
Time: '0', S5Time: '0', LTime: '0n'
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
function literalToValueExpr(dt, lit) {
|
|
197
|
+
if (lit == null) {
|
|
198
|
+
if (!(dt in DEFAULT_EXPR)) throw new Error(`No default value for datatype ${dt}`);
|
|
199
|
+
return DEFAULT_EXPR[dt];
|
|
200
|
+
}
|
|
201
|
+
switch (dt) {
|
|
202
|
+
case 'Bool':
|
|
203
|
+
return /true/i.test(lit) ? 'true' : 'false';
|
|
204
|
+
case 'Byte': case 'Word': case 'DWord':
|
|
205
|
+
return String(parseInt(lit.replace(/^16#/i, '').replace(/_/g, ''), 16));
|
|
206
|
+
case 'LWord':
|
|
207
|
+
return '0x' + lit.replace(/^16#/i, '').replace(/_/g, '').toUpperCase() + 'n';
|
|
208
|
+
case 'SInt': case 'Int': case 'DInt':
|
|
209
|
+
case 'USInt': case 'UInt': case 'UDInt':
|
|
210
|
+
return lit.replace(/_/g, '');
|
|
211
|
+
case 'LInt': case 'ULInt':
|
|
212
|
+
return lit.replace(/_/g, '') + 'n';
|
|
213
|
+
case 'Real': case 'LReal':
|
|
214
|
+
return lit.replace(/_/g, '');
|
|
215
|
+
case 'Char': case 'WChar': case 'String': case 'WString':
|
|
216
|
+
return JSON.stringify(quoted(lit));
|
|
217
|
+
case 'Date': {
|
|
218
|
+
const m = lit.match(/(\d{4})-(\d{2})-(\d{2})/);
|
|
219
|
+
return `new Date(${+m[1]}, ${+m[2] - 1}, ${+m[3]})`;
|
|
220
|
+
}
|
|
221
|
+
case 'TimeOfDay':
|
|
222
|
+
return todMs(lit);
|
|
223
|
+
case 'LTod':
|
|
224
|
+
return ltodNs(lit) + 'n';
|
|
225
|
+
case 'DateAndTime': case 'Dtl': case 'Ldt': {
|
|
226
|
+
const p = dateTimeParts(lit, 3);
|
|
227
|
+
return `new Date(Date.UTC(${p.y}, ${p.mo - 1}, ${p.d}, ${p.h}, ${p.mi}, ${p.s}, ${p.frac}))`;
|
|
228
|
+
}
|
|
229
|
+
case 'Time': case 'S5Time':
|
|
230
|
+
return durationMs(lit);
|
|
231
|
+
case 'LTime':
|
|
232
|
+
return durationNs(lit) + 'n';
|
|
233
|
+
default:
|
|
234
|
+
throw new Error(`No value converter for datatype ${dt}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Reads a .db source and returns its constant presets (everything except the
|
|
239
|
+
// *_write test symbol) with the raw assigned literal (or null for defaults).
|
|
240
|
+
function parseConstants(db) {
|
|
241
|
+
const file = path.join(__dirname, '..', 'plc', 's7-1500', `${db}.db`);
|
|
242
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
243
|
+
|
|
244
|
+
const varStart = text.search(/\n\s*VAR\b/);
|
|
245
|
+
const varEnd = text.indexOf('END_VAR');
|
|
246
|
+
const beginStart = text.indexOf('BEGIN');
|
|
247
|
+
if (varStart === -1 || varEnd === -1) {
|
|
248
|
+
throw new Error(`Cannot locate VAR block in ${db}.db`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Assigned values from the BEGIN block (skip struct-field assignments like
|
|
252
|
+
// "DTL_x.YEAR := ..."; the scalar "DTL_x := DTL#..." line wins).
|
|
253
|
+
const assigned = {};
|
|
254
|
+
if (beginStart !== -1) {
|
|
255
|
+
const beginBody = text.slice(beginStart);
|
|
256
|
+
for (const line of beginBody.split(/\r?\n/)) {
|
|
257
|
+
const m = line.match(/^\s*([A-Za-z_]\w*)\s*:=\s*(.+?);\s*$/);
|
|
258
|
+
if (m) assigned[m[1]] = m[2].trim();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const out = [];
|
|
263
|
+
for (const line of text.slice(varStart, varEnd).split(/\r?\n/)) {
|
|
264
|
+
const m = line.match(/^\s*([A-Za-z_]\w*)\s*(?:\{[^}]*\})?\s*:\s*([A-Za-z_]\w*)/);
|
|
265
|
+
if (!m) continue;
|
|
266
|
+
const name = m[1];
|
|
267
|
+
const plcType = m[2];
|
|
268
|
+
if (name === 'VAR' || /_write$/i.test(name)) continue;
|
|
269
|
+
const dt = PLC_TO_DT[plcType];
|
|
270
|
+
if (!dt) {
|
|
271
|
+
console.warn(`Skipping ${db}.${name}: unmapped PLC type ${plcType}`);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
out.push({ name, dt, value: literalToValueExpr(dt, assigned[name]) });
|
|
275
|
+
}
|
|
276
|
+
return out;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
// Build the TESTS dataset: one entry per *_write tag, with all constants of
|
|
281
|
+
// that datatype as test cases (in DB source order).
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
const TESTS = [];
|
|
285
|
+
for (const section of SECTIONS) {
|
|
286
|
+
const constants = parseConstants(section.db);
|
|
287
|
+
const groups = new Map();
|
|
288
|
+
for (const c of constants) {
|
|
289
|
+
if (!groups.has(c.dt)) groups.set(c.dt, []);
|
|
290
|
+
groups.get(c.dt).push(c);
|
|
291
|
+
}
|
|
292
|
+
for (const item of section.items) {
|
|
293
|
+
const members = groups.get(item.dt) || [];
|
|
294
|
+
if (!members.length) {
|
|
295
|
+
throw new Error(`No constants for ${section.db} datatype ${item.dt}`);
|
|
296
|
+
}
|
|
297
|
+
TESTS.push({
|
|
298
|
+
group: section.title,
|
|
299
|
+
datatype: item.dt,
|
|
300
|
+
symbol: `${section.db}.${item.var}`,
|
|
301
|
+
cases: members.map(c => ({ name: c.name, value: c.value }))
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
// Render the TESTS dataset as JS source for embedding in the function node.
|
|
308
|
+
// `value` is a raw JS expression (number / BigInt literal / Date / string),
|
|
309
|
+
// so it must NOT be JSON-encoded.
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
function renderTests(tests) {
|
|
313
|
+
const blocks = tests.map((t) => {
|
|
314
|
+
const cases = t.cases
|
|
315
|
+
.map(c => ` { name: ${JSON.stringify(c.name)}, value: ${c.value} }`)
|
|
316
|
+
.join(',\n');
|
|
317
|
+
return [
|
|
318
|
+
' {',
|
|
319
|
+
` group: ${JSON.stringify(t.group)},`,
|
|
320
|
+
` datatype: ${JSON.stringify(t.datatype)},`,
|
|
321
|
+
` symbol: ${JSON.stringify(t.symbol)},`,
|
|
322
|
+
' cases: [',
|
|
323
|
+
cases,
|
|
324
|
+
' ]',
|
|
325
|
+
' }'
|
|
326
|
+
].join('\n');
|
|
327
|
+
});
|
|
328
|
+
return '[\n' + blocks.join(',\n') + '\n]';
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
// Test Runner function node body. Sequential state machine driven by message
|
|
333
|
+
// feedback. msg._t carries the phase ('write' | 'read') and the current
|
|
334
|
+
// datatype/case indices; msg.symbols is a string[] (read node requirement);
|
|
335
|
+
// write uses msg.datatype for the single *_write tag. The write/read nodes
|
|
336
|
+
// preserve all msg fields except payload, and the catch node re-delivers the
|
|
337
|
+
// same msg with msg.error set.
|
|
338
|
+
// (Uses only string concatenation, never template literals.)
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
|
|
341
|
+
const RUNNER_BODY = `
|
|
342
|
+
// ---- comparators (read-back value vs. expected) ----
|
|
343
|
+
var eq = function (e, r) { return r === e; };
|
|
344
|
+
var bigeq = function (e, r) {
|
|
345
|
+
try { return (typeof r === 'bigint' ? r : BigInt(r)) === (typeof e === 'bigint' ? e : BigInt(e)); }
|
|
346
|
+
catch (_) { return false; }
|
|
347
|
+
};
|
|
348
|
+
var realeq = function (e, r) { return typeof r === 'number' && r === Math.fround(e); };
|
|
349
|
+
var lrealeq = function (e, r) { return r === e || (Number.isNaN(e) && Number.isNaN(r)); };
|
|
350
|
+
// Duck-type the Date (getTime) instead of using instanceof, so the comparison
|
|
351
|
+
// is robust even if the read-back Date crosses a vm realm boundary into the
|
|
352
|
+
// function-node sandbox (where instanceof Date can be false).
|
|
353
|
+
var dateeq = function (e, r) {
|
|
354
|
+
var te = e && typeof e.getTime === 'function' ? e.getTime() : Date.parse(e);
|
|
355
|
+
var tr = r && typeof r.getTime === 'function' ? r.getTime() : Date.parse(r);
|
|
356
|
+
return !isNaN(te) && te === tr;
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
var COMPARATORS = {
|
|
360
|
+
Bool: eq,
|
|
361
|
+
Byte: eq, Word: eq, DWord: eq, LWord: bigeq,
|
|
362
|
+
SInt: eq, Int: eq, DInt: eq, USInt: eq, UInt: eq, UDInt: eq,
|
|
363
|
+
LInt: bigeq, ULInt: bigeq,
|
|
364
|
+
Real: realeq, LReal: lrealeq,
|
|
365
|
+
Char: eq, WChar: eq, String: eq, WString: eq,
|
|
366
|
+
Date: dateeq, DateAndTime: dateeq, Ldt: dateeq, Dtl: dateeq,
|
|
367
|
+
TimeOfDay: eq, Time: eq, S5Time: eq,
|
|
368
|
+
LTod: bigeq, LTime: bigeq
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
function show(v) {
|
|
372
|
+
if (typeof v === 'bigint') return v.toString();
|
|
373
|
+
if (v && typeof v.toISOString === 'function') return v.toISOString();
|
|
374
|
+
return v;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function tagResult(payload, symbol) {
|
|
378
|
+
if (!payload || typeof payload !== 'object') return null;
|
|
379
|
+
if (payload[symbol]) return payload[symbol];
|
|
380
|
+
var keys = Object.keys(payload);
|
|
381
|
+
return keys.length ? payload[keys[0]] : null;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function newAcc() { return { passed: 0, failed: 0, cases: [] }; }
|
|
385
|
+
|
|
386
|
+
function buildWriteMsg(S) {
|
|
387
|
+
var t = TESTS[S.di];
|
|
388
|
+
var c = t.cases[S.ci];
|
|
389
|
+
return {
|
|
390
|
+
_t: { phase: 'write', di: S.di, ci: S.ci },
|
|
391
|
+
symbols: [t.symbol],
|
|
392
|
+
datatype: t.datatype,
|
|
393
|
+
payload: c.value
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function buildReadMsg(S) {
|
|
398
|
+
var t = TESTS[S.di];
|
|
399
|
+
return {
|
|
400
|
+
_t: { phase: 'read', di: S.di, ci: S.ci },
|
|
401
|
+
symbols: [t.symbol],
|
|
402
|
+
payload: ''
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function record(S, t, c, ok, wrote, read, error) {
|
|
407
|
+
S.cur.cases.push({
|
|
408
|
+
name: c.name,
|
|
409
|
+
ok: ok,
|
|
410
|
+
wrote: show(wrote),
|
|
411
|
+
read: read === null || read === undefined ? null : show(read),
|
|
412
|
+
error: error || ''
|
|
413
|
+
});
|
|
414
|
+
if (ok) { S.cur.passed++; S.totalPassed++; } else { S.cur.failed++; S.totalFailed++; }
|
|
415
|
+
S.totalCases++;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Advance to the next case; emit a per-datatype result when a datatype is
|
|
419
|
+
// finished and a final summary when all datatypes are done.
|
|
420
|
+
function advance(S) {
|
|
421
|
+
var t = TESTS[S.di];
|
|
422
|
+
S.ci++;
|
|
423
|
+
if (S.ci < t.cases.length) {
|
|
424
|
+
context.set('state', S);
|
|
425
|
+
return [buildWriteMsg(S), null, null, null];
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
var result = {
|
|
429
|
+
topic: t.group + '/' + t.datatype,
|
|
430
|
+
payload: {
|
|
431
|
+
group: t.group,
|
|
432
|
+
datatype: t.datatype,
|
|
433
|
+
symbol: t.symbol,
|
|
434
|
+
total: t.cases.length,
|
|
435
|
+
passed: S.cur.passed,
|
|
436
|
+
failed: S.cur.failed,
|
|
437
|
+
cases: S.cur.cases
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
S.results.push({ group: t.group, datatype: t.datatype, total: t.cases.length, passed: S.cur.passed, failed: S.cur.failed });
|
|
441
|
+
node.status({ fill: S.cur.failed ? 'yellow' : 'green', shape: 'dot', text: t.datatype + ': ' + S.cur.passed + '/' + t.cases.length + ' ok' });
|
|
442
|
+
|
|
443
|
+
S.di++;
|
|
444
|
+
S.ci = 0;
|
|
445
|
+
if (S.di < TESTS.length) {
|
|
446
|
+
S.cur = newAcc();
|
|
447
|
+
context.set('state', S);
|
|
448
|
+
return [buildWriteMsg(S), null, result, null];
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
S.running = false;
|
|
452
|
+
context.set('state', S);
|
|
453
|
+
var summary = {
|
|
454
|
+
topic: 'SUMMARY',
|
|
455
|
+
payload: {
|
|
456
|
+
totalDatatypes: TESTS.length,
|
|
457
|
+
totalCases: S.totalCases,
|
|
458
|
+
passed: S.totalPassed,
|
|
459
|
+
failed: S.totalFailed,
|
|
460
|
+
failures: S.results.filter(function (r) { return r.failed > 0; })
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
node.status({ fill: S.totalFailed ? 'red' : 'green', shape: 'dot', text: 'done: ' + S.totalPassed + '/' + S.totalCases + ' ok' });
|
|
464
|
+
return [null, null, result, summary];
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
var S = context.get('state');
|
|
468
|
+
|
|
469
|
+
// Start trigger (inject): no _t marker.
|
|
470
|
+
if (!msg || !msg._t) {
|
|
471
|
+
if (S && S.running) {
|
|
472
|
+
node.warn('R/W test already running, ignoring start trigger');
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
S = {
|
|
476
|
+
running: true,
|
|
477
|
+
di: 0,
|
|
478
|
+
ci: 0,
|
|
479
|
+
cur: newAcc(),
|
|
480
|
+
results: [],
|
|
481
|
+
totalCases: 0,
|
|
482
|
+
totalPassed: 0,
|
|
483
|
+
totalFailed: 0
|
|
484
|
+
};
|
|
485
|
+
context.set('state', S);
|
|
486
|
+
node.status({ fill: 'blue', shape: 'dot', text: 'running...' });
|
|
487
|
+
return [buildWriteMsg(S), null, null, null];
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Stray feedback (e.g. after a completed run) is ignored.
|
|
491
|
+
if (!S || !S.running) {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
var t = TESTS[S.di];
|
|
496
|
+
var c = t.cases[S.ci];
|
|
497
|
+
var phase = msg._t.phase;
|
|
498
|
+
var errText = msg.error && msg.error.message ? msg.error.message : null;
|
|
499
|
+
|
|
500
|
+
if (phase === 'write') {
|
|
501
|
+
var wr = tagResult(msg.payload, t.symbol);
|
|
502
|
+
if (errText || !wr || wr.status !== 'ok') {
|
|
503
|
+
record(S, t, c, false, c.value, null, errText || (wr && wr.error) || 'write failed');
|
|
504
|
+
return advance(S);
|
|
505
|
+
}
|
|
506
|
+
// write ok -> read the same tag back
|
|
507
|
+
return [null, buildReadMsg(S), null, null];
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (phase === 'read') {
|
|
511
|
+
var rd = tagResult(msg.payload, t.symbol);
|
|
512
|
+
if (errText || !rd || rd.status !== 'ok') {
|
|
513
|
+
record(S, t, c, false, c.value, null, errText || (rd && rd.error) || 'read failed');
|
|
514
|
+
return advance(S);
|
|
515
|
+
}
|
|
516
|
+
var cmp = COMPARATORS[t.datatype] || eq;
|
|
517
|
+
var ok = cmp(c.value, rd.value);
|
|
518
|
+
var note = ok ? '' : 'value mismatch';
|
|
519
|
+
record(S, t, c, ok, c.value, rd.value, note);
|
|
520
|
+
return advance(S);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return null;
|
|
524
|
+
`;
|
|
525
|
+
|
|
526
|
+
const RUNNER_FUNC = 'const TESTS = ' + renderTests(TESTS) + ';\n' + RUNNER_BODY;
|
|
527
|
+
|
|
528
|
+
// ---------------------------------------------------------------------------
|
|
529
|
+
// Assemble the flow nodes.
|
|
530
|
+
// ---------------------------------------------------------------------------
|
|
531
|
+
|
|
532
|
+
const RUNNER_ID = 'rw_test_runner';
|
|
533
|
+
const WRITE_ID = 'rw_test_write';
|
|
534
|
+
const READ_ID = 'rw_test_read';
|
|
535
|
+
const CATCH_ID = 'rw_test_catch';
|
|
536
|
+
const DEBUG_RESULT_ID = 'rw_test_dbg_result';
|
|
537
|
+
const DEBUG_SUMMARY_ID = 'rw_test_dbg_summary';
|
|
538
|
+
const INJECT_ID = 'rw_test_start';
|
|
539
|
+
|
|
540
|
+
const nodes = [];
|
|
541
|
+
|
|
542
|
+
nodes.push({
|
|
543
|
+
id: TAB,
|
|
544
|
+
type: 'tab',
|
|
545
|
+
label: 'R/W Verification Test',
|
|
546
|
+
disabled: false,
|
|
547
|
+
info: 'Automated read/write verification for all scalar datatypes in '
|
|
548
|
+
+ 'plc/s7-1500.\n'
|
|
549
|
+
+ 'Press the "Run all R/W tests" inject. The Test Runner writes every '
|
|
550
|
+
+ 'constant value of every datatype into the matching *_write tag, '
|
|
551
|
+
+ 'reads it back and compares write vs. read-back.\n'
|
|
552
|
+
+ 'The run never aborts on a failure: it completes fully and emits one '
|
|
553
|
+
+ 'result per datatype ("Datatype Result") plus a final "Summary".\n'
|
|
554
|
+
+ 'DTL round-trips as Date (ms resolution).\n'
|
|
555
|
+
+ 'Regenerate with: node scripts/generate-rw-test-flow.js'
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
nodes.push({
|
|
559
|
+
id: 'rw_test_hdr',
|
|
560
|
+
type: 'comment',
|
|
561
|
+
z: TAB,
|
|
562
|
+
name: 'R/W verification — write, read back and compare every scalar constant',
|
|
563
|
+
info: '',
|
|
564
|
+
x: 320,
|
|
565
|
+
y: 40,
|
|
566
|
+
wires: []
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
nodes.push({
|
|
570
|
+
id: INJECT_ID,
|
|
571
|
+
type: 'inject',
|
|
572
|
+
z: TAB,
|
|
573
|
+
name: 'Run all R/W tests',
|
|
574
|
+
props: [{ p: 'payload' }],
|
|
575
|
+
repeat: '',
|
|
576
|
+
crontab: '',
|
|
577
|
+
once: false,
|
|
578
|
+
onceDelay: 0.1,
|
|
579
|
+
topic: '',
|
|
580
|
+
payload: '',
|
|
581
|
+
payloadType: 'date',
|
|
582
|
+
x: 150,
|
|
583
|
+
y: 140,
|
|
584
|
+
wires: [[RUNNER_ID]]
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
nodes.push({
|
|
588
|
+
id: RUNNER_ID,
|
|
589
|
+
type: 'function',
|
|
590
|
+
z: TAB,
|
|
591
|
+
name: 'Test Runner',
|
|
592
|
+
func: RUNNER_FUNC,
|
|
593
|
+
outputs: 4,
|
|
594
|
+
timeout: 0,
|
|
595
|
+
noerr: 0,
|
|
596
|
+
initialize: '',
|
|
597
|
+
finalize: '',
|
|
598
|
+
libs: [],
|
|
599
|
+
x: 410,
|
|
600
|
+
y: 140,
|
|
601
|
+
wires: [
|
|
602
|
+
[WRITE_ID],
|
|
603
|
+
[READ_ID],
|
|
604
|
+
[DEBUG_RESULT_ID],
|
|
605
|
+
[DEBUG_SUMMARY_ID]
|
|
606
|
+
]
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
nodes.push({
|
|
610
|
+
id: WRITE_ID,
|
|
611
|
+
type: 's7-plus write',
|
|
612
|
+
z: TAB,
|
|
613
|
+
name: 'write *_write',
|
|
614
|
+
endpoint: EP,
|
|
615
|
+
symbols: [],
|
|
616
|
+
outputFormat: DEFAULT_OUTPUT_FORMAT,
|
|
617
|
+
x: 690,
|
|
618
|
+
y: 100,
|
|
619
|
+
wires: [[RUNNER_ID]]
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
nodes.push({
|
|
623
|
+
id: READ_ID,
|
|
624
|
+
type: 's7-plus read',
|
|
625
|
+
z: TAB,
|
|
626
|
+
name: 'read *_write',
|
|
627
|
+
endpoint: EP,
|
|
628
|
+
symbols: [],
|
|
629
|
+
outputFormat: DEFAULT_OUTPUT_FORMAT,
|
|
630
|
+
x: 690,
|
|
631
|
+
y: 180,
|
|
632
|
+
wires: [[RUNNER_ID]]
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
nodes.push({
|
|
636
|
+
id: CATCH_ID,
|
|
637
|
+
type: 'catch',
|
|
638
|
+
z: TAB,
|
|
639
|
+
name: 'write/read errors',
|
|
640
|
+
scope: [WRITE_ID, READ_ID],
|
|
641
|
+
uncaught: false,
|
|
642
|
+
x: 410,
|
|
643
|
+
y: 260,
|
|
644
|
+
wires: [[RUNNER_ID]]
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
nodes.push({
|
|
648
|
+
id: DEBUG_RESULT_ID,
|
|
649
|
+
type: 'debug',
|
|
650
|
+
z: TAB,
|
|
651
|
+
name: 'Datatype Result',
|
|
652
|
+
active: true,
|
|
653
|
+
tosidebar: true,
|
|
654
|
+
console: false,
|
|
655
|
+
tostatus: false,
|
|
656
|
+
complete: 'payload',
|
|
657
|
+
targetType: 'msg',
|
|
658
|
+
statusVal: '',
|
|
659
|
+
statusType: 'auto',
|
|
660
|
+
x: 720,
|
|
661
|
+
y: 280,
|
|
662
|
+
wires: []
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
nodes.push({
|
|
666
|
+
id: DEBUG_SUMMARY_ID,
|
|
667
|
+
type: 'debug',
|
|
668
|
+
z: TAB,
|
|
669
|
+
name: 'Summary',
|
|
670
|
+
active: true,
|
|
671
|
+
tosidebar: true,
|
|
672
|
+
console: false,
|
|
673
|
+
tostatus: false,
|
|
674
|
+
complete: 'payload',
|
|
675
|
+
targetType: 'msg',
|
|
676
|
+
statusVal: '',
|
|
677
|
+
statusType: 'auto',
|
|
678
|
+
x: 700,
|
|
679
|
+
y: 340,
|
|
680
|
+
wires: []
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
nodes.push(endpointNode());
|
|
684
|
+
nodes.push(globalConfigNode('0.0.1'));
|
|
685
|
+
|
|
686
|
+
const out = path.join(__dirname, '..', 'examples', 'read-write-test-flow.json');
|
|
687
|
+
fs.writeFileSync(out, JSON.stringify(nodes, null, 4) + '\n');
|
|
688
|
+
|
|
689
|
+
const totalCases = TESTS.reduce((n, t) => n + t.cases.length, 0);
|
|
690
|
+
console.log(`Wrote ${nodes.length} nodes (${TESTS.length} datatypes, ${totalCases} test cases) to ${out}`);
|