node-red-contrib-event-calc 3.3.6 → 3.3.15
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.md +100 -62
- package/examples/alarm-simulation-example.json +288 -0
- package/examples/isa88-batch-example.json +278 -0
- package/examples/opcua-alarm-example.json +516 -0
- package/examples/test-flows.json +723 -0
- package/examples/throughput-test.json +293 -0
- package/nodes/event-alarm.html +252 -0
- package/nodes/event-alarm.js +292 -0
- package/nodes/event-cache.js +72 -37
- package/nodes/event-calc.js +114 -124
- package/nodes/event-frame.html +192 -0
- package/nodes/event-frame.js +246 -0
- package/nodes/simple-frame.html +125 -0
- package/nodes/simple-frame.js +126 -0
- package/package.json +5 -2
package/nodes/event-calc.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - Maps variables to exact topics
|
|
6
6
|
* - Evaluates JavaScript expressions when inputs update
|
|
7
7
|
* - Trigger modes: 'any' (any input updates) or 'all' (all inputs have values)
|
|
8
|
-
* -
|
|
8
|
+
* - Cached compiled functions for high throughput
|
|
9
9
|
* - Dynamic expression update via input message
|
|
10
10
|
* - Built-in helper functions for common operations
|
|
11
11
|
*/
|
|
@@ -16,17 +16,17 @@ module.exports = function(RED) {
|
|
|
16
16
|
// Math shortcuts
|
|
17
17
|
min: (...args) => Math.min(...args.flat()),
|
|
18
18
|
max: (...args) => Math.max(...args.flat()),
|
|
19
|
-
abs:
|
|
20
|
-
sqrt:
|
|
21
|
-
pow:
|
|
22
|
-
log:
|
|
23
|
-
log10:
|
|
24
|
-
exp:
|
|
25
|
-
floor:
|
|
26
|
-
ceil:
|
|
27
|
-
sin:
|
|
28
|
-
cos:
|
|
29
|
-
tan:
|
|
19
|
+
abs: Math.abs,
|
|
20
|
+
sqrt: Math.sqrt,
|
|
21
|
+
pow: Math.pow,
|
|
22
|
+
log: Math.log,
|
|
23
|
+
log10: Math.log10,
|
|
24
|
+
exp: Math.exp,
|
|
25
|
+
floor: Math.floor,
|
|
26
|
+
ceil: Math.ceil,
|
|
27
|
+
sin: Math.sin,
|
|
28
|
+
cos: Math.cos,
|
|
29
|
+
tan: Math.tan,
|
|
30
30
|
PI: Math.PI,
|
|
31
31
|
E: Math.E,
|
|
32
32
|
|
|
@@ -74,6 +74,11 @@ module.exports = function(RED) {
|
|
|
74
74
|
: h >= startHour || h < endHour; // wraps midnight
|
|
75
75
|
}
|
|
76
76
|
};
|
|
77
|
+
|
|
78
|
+
// Pre-compute helper keys (shared across all nodes, never changes)
|
|
79
|
+
const helperKeys = Object.keys(helpers);
|
|
80
|
+
const helperValues = Object.values(helpers);
|
|
81
|
+
|
|
77
82
|
function EventCalcNode(config) {
|
|
78
83
|
RED.nodes.createNode(this, config);
|
|
79
84
|
const node = this;
|
|
@@ -111,21 +116,89 @@ module.exports = function(RED) {
|
|
|
111
116
|
}
|
|
112
117
|
}
|
|
113
118
|
|
|
119
|
+
// Subscribe to inputs
|
|
120
|
+
const latestValues = new Map();
|
|
121
|
+
|
|
122
|
+
// Dynamic helpers that need access to cache (created per-node)
|
|
123
|
+
const cacheHelpers = {
|
|
124
|
+
now: Date.now,
|
|
125
|
+
hasChanged: (varName) => {
|
|
126
|
+
const data = latestValues.get(varName);
|
|
127
|
+
if (!data || !data.topic) return false;
|
|
128
|
+
const entry = node.cacheConfig.getValue(data.topic);
|
|
129
|
+
if (!entry || !entry.previous) return false;
|
|
130
|
+
return entry.value !== entry.previous.value;
|
|
131
|
+
},
|
|
132
|
+
timeSinceLastChange: (varName) => {
|
|
133
|
+
const data = latestValues.get(varName);
|
|
134
|
+
if (!data || !data.topic) return 0;
|
|
135
|
+
const entry = node.cacheConfig.getValue(data.topic);
|
|
136
|
+
if (!entry) return 0;
|
|
137
|
+
if (!entry.previous || entry.value === entry.previous.value) {
|
|
138
|
+
return Date.now() - (entry.previous ? entry.previous.ts : entry.ts);
|
|
139
|
+
}
|
|
140
|
+
return Date.now() - entry.ts;
|
|
141
|
+
},
|
|
142
|
+
prev: (varName) => {
|
|
143
|
+
const data = latestValues.get(varName);
|
|
144
|
+
if (!data || !data.topic) return undefined;
|
|
145
|
+
const prev = node.cacheConfig.getPrevious(data.topic);
|
|
146
|
+
return prev ? prev.value : undefined;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// Pre-compute cache helper keys
|
|
151
|
+
const cacheHelperKeys = Object.keys(cacheHelpers);
|
|
152
|
+
const cacheHelperValues = Object.values(cacheHelpers);
|
|
153
|
+
|
|
154
|
+
// Pre-compute input variable names (order is stable)
|
|
155
|
+
const inputNames = node.inputMappings.map(m => m.name);
|
|
156
|
+
|
|
157
|
+
// --- Compiled function cache ---
|
|
158
|
+
// All param names = helpers + cacheHelpers + input variable names
|
|
159
|
+
// helpers and cacheHelpers are fixed; input names are fixed per node
|
|
160
|
+
const allParamNames = [...helperKeys, ...cacheHelperKeys, ...inputNames];
|
|
161
|
+
// Pre-allocate the values array (reused on every call)
|
|
162
|
+
const allParamValues = new Array(allParamNames.length);
|
|
163
|
+
// Fill the fixed portion (helpers + cacheHelpers)
|
|
164
|
+
const fixedCount = helperKeys.length + cacheHelperKeys.length;
|
|
165
|
+
for (let i = 0; i < helperKeys.length; i++) {
|
|
166
|
+
allParamValues[i] = helperValues[i];
|
|
167
|
+
}
|
|
168
|
+
for (let i = 0; i < cacheHelperKeys.length; i++) {
|
|
169
|
+
allParamValues[helperKeys.length + i] = cacheHelperValues[i];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Compiled function + expression it was compiled from
|
|
173
|
+
let compiledFn = null;
|
|
174
|
+
let compiledExpression = '';
|
|
175
|
+
|
|
176
|
+
function compileExpression(expr) {
|
|
177
|
+
if (expr === compiledExpression && compiledFn) return compiledFn;
|
|
178
|
+
compiledFn = new Function(...allParamNames, `return ${expr};`);
|
|
179
|
+
compiledExpression = expr;
|
|
180
|
+
return compiledFn;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Compile initial expression
|
|
184
|
+
try {
|
|
185
|
+
compileExpression(node.expression);
|
|
186
|
+
} catch (err) {
|
|
187
|
+
node.status({ fill: "red", shape: "ring", text: "compile error" });
|
|
188
|
+
}
|
|
189
|
+
|
|
114
190
|
/**
|
|
115
191
|
* Attempt to calculate and output result
|
|
116
|
-
* @param {string} triggerTopic - Topic that triggered the calculation
|
|
117
|
-
* @param {Map} latestValues - Current cached values
|
|
118
|
-
* @param {number} triggerTs - Timestamp of the triggering event
|
|
119
192
|
*/
|
|
120
|
-
function tryCalculate(triggerTopic,
|
|
193
|
+
function tryCalculate(triggerTopic, triggerTs) {
|
|
121
194
|
// Ignore updates triggered by our own output
|
|
122
195
|
if (triggerTopic === node.outputTopic) {
|
|
123
196
|
return;
|
|
124
197
|
}
|
|
125
198
|
|
|
126
199
|
if (node.triggerOn === 'all') {
|
|
127
|
-
for (
|
|
128
|
-
if (!latestValues.has(
|
|
200
|
+
for (let i = 0; i < inputNames.length; i++) {
|
|
201
|
+
if (!latestValues.has(inputNames[i])) {
|
|
129
202
|
return;
|
|
130
203
|
}
|
|
131
204
|
}
|
|
@@ -135,72 +208,45 @@ module.exports = function(RED) {
|
|
|
135
208
|
return;
|
|
136
209
|
}
|
|
137
210
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
for (const input of node.inputMappings) {
|
|
143
|
-
const data = latestValues.get(input.name);
|
|
211
|
+
// Fill input variable values into the pre-allocated array
|
|
212
|
+
let hasAllInputs = true;
|
|
213
|
+
for (let i = 0; i < inputNames.length; i++) {
|
|
214
|
+
const data = latestValues.get(inputNames[i]);
|
|
144
215
|
if (data && data.value !== undefined && data.value !== null) {
|
|
145
|
-
|
|
146
|
-
inputDetails[input.name] = {
|
|
147
|
-
topic: data.topic,
|
|
148
|
-
value: data.value,
|
|
149
|
-
ts: data.ts
|
|
150
|
-
};
|
|
216
|
+
allParamValues[fixedCount + i] = data.value;
|
|
151
217
|
} else {
|
|
152
|
-
|
|
153
|
-
|
|
218
|
+
allParamValues[fixedCount + i] = undefined;
|
|
219
|
+
hasAllInputs = false;
|
|
154
220
|
}
|
|
155
221
|
}
|
|
156
222
|
|
|
157
|
-
// Build topics mapping: variable name -> topic
|
|
158
|
-
const topics = { _output: node.outputTopic };
|
|
159
|
-
const timestamps = {};
|
|
160
|
-
for (const [name, details] of Object.entries(inputDetails)) {
|
|
161
|
-
topics[name] = details.topic;
|
|
162
|
-
timestamps[name] = details.ts;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
223
|
try {
|
|
166
|
-
const
|
|
167
|
-
const
|
|
168
|
-
const paramValues = Object.values(allParams);
|
|
169
|
-
|
|
170
|
-
const fn = new Function(...paramNames, `return ${node.expression};`);
|
|
171
|
-
const result = fn(...paramValues);
|
|
224
|
+
const fn = compileExpression(node.expression);
|
|
225
|
+
const result = fn(...allParamValues);
|
|
172
226
|
|
|
173
227
|
// Check for NaN or invalid result
|
|
174
228
|
if (typeof result === 'number' && isNaN(result)) {
|
|
175
|
-
|
|
229
|
+
node.send([null, {
|
|
176
230
|
topic: node.outputTopic,
|
|
177
|
-
payload: {
|
|
178
|
-
error: 'Expression resulted in NaN',
|
|
179
|
-
missingInputs: missingInputs,
|
|
180
|
-
expression: node.expression
|
|
181
|
-
},
|
|
231
|
+
payload: { error: 'Expression resulted in NaN', expression: node.expression },
|
|
182
232
|
trigger: triggerTopic,
|
|
183
233
|
ts: triggerTs
|
|
184
|
-
};
|
|
185
|
-
node.send([null, errorMsg]);
|
|
234
|
+
}]);
|
|
186
235
|
node.status({ fill: "yellow", shape: "ring", text: "NaN" });
|
|
187
236
|
return;
|
|
188
237
|
}
|
|
189
238
|
|
|
190
|
-
|
|
239
|
+
node.send([{
|
|
191
240
|
topic: node.outputTopic,
|
|
192
241
|
payload: result,
|
|
193
242
|
expression: node.expression,
|
|
194
243
|
trigger: triggerTopic,
|
|
195
244
|
ts: triggerTs
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
node.send([msg, null]);
|
|
245
|
+
}, null]);
|
|
199
246
|
|
|
200
247
|
node.cacheConfig.setValue(node.outputTopic, result, {
|
|
201
248
|
source: 'event-calc',
|
|
202
|
-
expression: node.expression
|
|
203
|
-
inputs: Object.keys(inputDetails)
|
|
249
|
+
expression: node.expression
|
|
204
250
|
});
|
|
205
251
|
|
|
206
252
|
const resultStr = String(result);
|
|
@@ -208,72 +254,16 @@ module.exports = function(RED) {
|
|
|
208
254
|
node.status({ fill: "green", shape: "dot", text: `= ${displayResult}` });
|
|
209
255
|
|
|
210
256
|
} catch (err) {
|
|
211
|
-
|
|
257
|
+
node.send([null, {
|
|
212
258
|
topic: node.outputTopic,
|
|
213
|
-
payload: {
|
|
214
|
-
error: err.message,
|
|
215
|
-
expression: node.expression,
|
|
216
|
-
context: context
|
|
217
|
-
},
|
|
259
|
+
payload: { error: err.message, expression: node.expression },
|
|
218
260
|
trigger: triggerTopic,
|
|
219
261
|
ts: triggerTs
|
|
220
|
-
};
|
|
221
|
-
node.send([null, errorMsg]);
|
|
262
|
+
}]);
|
|
222
263
|
node.status({ fill: "red", shape: "ring", text: "eval error" });
|
|
223
264
|
}
|
|
224
265
|
}
|
|
225
266
|
|
|
226
|
-
// Dynamic helpers that need access to cache (created per-node)
|
|
227
|
-
const cacheHelpers = {
|
|
228
|
-
/**
|
|
229
|
-
* now() - Returns current timestamp in milliseconds
|
|
230
|
-
*/
|
|
231
|
-
now: () => Date.now(),
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* hasChanged(varName) - Returns true if the variable's current value
|
|
235
|
-
* differs from its previous value. Returns false on first message.
|
|
236
|
-
*/
|
|
237
|
-
hasChanged: (varName) => {
|
|
238
|
-
const data = latestValues.get(varName);
|
|
239
|
-
if (!data || !data.topic) return false;
|
|
240
|
-
const entry = node.cacheConfig.getValue(data.topic);
|
|
241
|
-
if (!entry || !entry.previous) return false;
|
|
242
|
-
return entry.value !== entry.previous.value;
|
|
243
|
-
},
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* timeSinceLastChange(varName) - Returns milliseconds since the value
|
|
247
|
-
* last changed. If never changed, returns time since first message.
|
|
248
|
-
*/
|
|
249
|
-
timeSinceLastChange: (varName) => {
|
|
250
|
-
const data = latestValues.get(varName);
|
|
251
|
-
if (!data || !data.topic) return 0;
|
|
252
|
-
const entry = node.cacheConfig.getValue(data.topic);
|
|
253
|
-
if (!entry) return 0;
|
|
254
|
-
if (!entry.previous || entry.value === entry.previous.value) {
|
|
255
|
-
// Value hasn't changed - return time since previous timestamp
|
|
256
|
-
// (which is when the last different value was set)
|
|
257
|
-
return Date.now() - (entry.previous ? entry.previous.ts : entry.ts);
|
|
258
|
-
}
|
|
259
|
-
// Value just changed - return time since this update
|
|
260
|
-
return Date.now() - entry.ts;
|
|
261
|
-
},
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* prev(varName) - Returns the previous value for a variable
|
|
265
|
-
*/
|
|
266
|
-
prev: (varName) => {
|
|
267
|
-
const data = latestValues.get(varName);
|
|
268
|
-
if (!data || !data.topic) return undefined;
|
|
269
|
-
const prev = node.cacheConfig.getPrevious(data.topic);
|
|
270
|
-
return prev ? prev.value : undefined;
|
|
271
|
-
}
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
// Subscribe to inputs
|
|
275
|
-
const latestValues = new Map();
|
|
276
|
-
|
|
277
267
|
for (const input of node.inputMappings) {
|
|
278
268
|
const topicName = input.topic || input.pattern;
|
|
279
269
|
if (!input.name || !topicName) continue;
|
|
@@ -284,8 +274,7 @@ module.exports = function(RED) {
|
|
|
284
274
|
value: entry.value,
|
|
285
275
|
ts: entry.ts
|
|
286
276
|
});
|
|
287
|
-
|
|
288
|
-
tryCalculate(topic, latestValues, entry.ts);
|
|
277
|
+
tryCalculate(topic, entry.ts);
|
|
289
278
|
});
|
|
290
279
|
subscriptionIds.push(subId);
|
|
291
280
|
}
|
|
@@ -301,15 +290,16 @@ module.exports = function(RED) {
|
|
|
301
290
|
// Allow expression update via message
|
|
302
291
|
if (msg.expression && typeof msg.expression === 'string') {
|
|
303
292
|
node.expression = msg.expression;
|
|
293
|
+
compiledFn = null; // Force recompile
|
|
294
|
+
compiledExpression = '';
|
|
304
295
|
node.status({ fill: "blue", shape: "dot", text: "expr updated" });
|
|
305
296
|
}
|
|
306
297
|
|
|
307
298
|
// External trigger: any incoming message triggers calculation
|
|
308
299
|
if (node.externalTrigger) {
|
|
309
300
|
const triggerSource = msg.topic || '_external';
|
|
310
|
-
// Use the incoming message's timestamp or current time
|
|
311
301
|
const triggerTs = msg.timestamp || Date.now();
|
|
312
|
-
tryCalculate(triggerSource,
|
|
302
|
+
tryCalculate(triggerSource, triggerTs);
|
|
313
303
|
done();
|
|
314
304
|
return;
|
|
315
305
|
}
|
|
@@ -318,7 +308,7 @@ module.exports = function(RED) {
|
|
|
318
308
|
if (msg.payload === 'recalc' || msg.topic === 'recalc') {
|
|
319
309
|
if (latestValues.size > 0) {
|
|
320
310
|
const triggerTs = msg.timestamp || Date.now();
|
|
321
|
-
tryCalculate('_recalc',
|
|
311
|
+
tryCalculate('_recalc', triggerTs);
|
|
322
312
|
}
|
|
323
313
|
}
|
|
324
314
|
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.event-calc-white-text { fill: #ffffff !important; }
|
|
3
|
+
.red-ui-palette-node[data-palette-type="event-frame"] .red-ui-palette-label { color: #ffffff !important; }
|
|
4
|
+
</style>
|
|
5
|
+
|
|
6
|
+
<script type="text/javascript">
|
|
7
|
+
RED.nodes.registerType('event-frame', {
|
|
8
|
+
category: 'event calc',
|
|
9
|
+
color: '#8B6914',
|
|
10
|
+
defaults: {
|
|
11
|
+
name: { value: "" },
|
|
12
|
+
level: { value: "operation" },
|
|
13
|
+
batchName: { value: "" },
|
|
14
|
+
batchNameType: { value: "str" },
|
|
15
|
+
unit: { value: "" },
|
|
16
|
+
unitType: { value: "str" },
|
|
17
|
+
batchId: { value: "" },
|
|
18
|
+
batchIdType: { value: "str" },
|
|
19
|
+
parentLevel: { value: "" },
|
|
20
|
+
triggerField: { value: "payload" },
|
|
21
|
+
metadataField: { value: "" },
|
|
22
|
+
contextKey: { value: "isa88_batches" },
|
|
23
|
+
endChildren: { value: true }
|
|
24
|
+
},
|
|
25
|
+
inputs: 1,
|
|
26
|
+
outputs: 2,
|
|
27
|
+
outputLabels: ["start", "end"],
|
|
28
|
+
icon: "font-awesome/fa-industry",
|
|
29
|
+
label: function() {
|
|
30
|
+
return this.name || this.level || "event frame";
|
|
31
|
+
},
|
|
32
|
+
paletteLabel: "event frame",
|
|
33
|
+
labelStyle: function() { return (this.name ? "node_label_italic" : "") + " event-calc-white-text"; },
|
|
34
|
+
oneditprepare: function() {
|
|
35
|
+
var node = this;
|
|
36
|
+
// Typed inputs for dynamic values
|
|
37
|
+
$("#node-input-batchName").typedInput({
|
|
38
|
+
default: 'str',
|
|
39
|
+
types: ['str', 'msg', 'flow', 'global', 'env'],
|
|
40
|
+
typeField: "#node-input-batchNameType"
|
|
41
|
+
});
|
|
42
|
+
$("#node-input-unit").typedInput({
|
|
43
|
+
default: 'str',
|
|
44
|
+
types: ['str', 'msg', 'flow', 'global', 'env'],
|
|
45
|
+
typeField: "#node-input-unitType"
|
|
46
|
+
});
|
|
47
|
+
$("#node-input-batchId").typedInput({
|
|
48
|
+
default: 'str',
|
|
49
|
+
types: ['str', 'msg', 'flow', 'global', 'env'],
|
|
50
|
+
typeField: "#node-input-batchIdType"
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<script type="text/html" data-template-name="event-frame">
|
|
57
|
+
<div class="form-row">
|
|
58
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
59
|
+
<input type="text" id="node-input-name" placeholder="Name">
|
|
60
|
+
</div>
|
|
61
|
+
<div class="form-row">
|
|
62
|
+
<label for="node-input-level"><i class="fa fa-sitemap"></i> ISA-88 Level</label>
|
|
63
|
+
<select id="node-input-level">
|
|
64
|
+
<option value="procedure">Procedure</option>
|
|
65
|
+
<option value="unit_procedure">Unit Procedure</option>
|
|
66
|
+
<option value="operation">Operation</option>
|
|
67
|
+
<option value="phase">Phase</option>
|
|
68
|
+
</select>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="form-row">
|
|
71
|
+
<label for="node-input-parentLevel"><i class="fa fa-level-up"></i> Parent Level</label>
|
|
72
|
+
<select id="node-input-parentLevel">
|
|
73
|
+
<option value="">(none)</option>
|
|
74
|
+
<option value="procedure">Procedure</option>
|
|
75
|
+
<option value="unit_procedure">Unit Procedure</option>
|
|
76
|
+
<option value="operation">Operation</option>
|
|
77
|
+
<option value="phase">Phase</option>
|
|
78
|
+
</select>
|
|
79
|
+
<div class="form-tips">Links this record's <code>parent_id</code> to the currently active record at the selected level.</div>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="form-row">
|
|
82
|
+
<label for="node-input-triggerField"><i class="fa fa-bolt"></i> Trigger Field</label>
|
|
83
|
+
<input type="text" id="node-input-triggerField" placeholder="payload">
|
|
84
|
+
<div class="form-tips">Message property that is <code>true</code> to start and <code>false</code> to end.</div>
|
|
85
|
+
</div>
|
|
86
|
+
<div class="form-row">
|
|
87
|
+
<label for="node-input-batchName"><i class="fa fa-pencil"></i> Batch Name</label>
|
|
88
|
+
<input type="text" id="node-input-batchName" placeholder="optional">
|
|
89
|
+
<input type="hidden" id="node-input-batchNameType">
|
|
90
|
+
</div>
|
|
91
|
+
<div class="form-row">
|
|
92
|
+
<label for="node-input-batchId"><i class="fa fa-barcode"></i> Batch ID</label>
|
|
93
|
+
<input type="text" id="node-input-batchId" placeholder="optional">
|
|
94
|
+
<input type="hidden" id="node-input-batchIdType">
|
|
95
|
+
</div>
|
|
96
|
+
<div class="form-row">
|
|
97
|
+
<label for="node-input-unit"><i class="fa fa-cube"></i> Unit</label>
|
|
98
|
+
<input type="text" id="node-input-unit" placeholder="optional">
|
|
99
|
+
<input type="hidden" id="node-input-unitType">
|
|
100
|
+
</div>
|
|
101
|
+
<div class="form-row">
|
|
102
|
+
<label for="node-input-metadataField"><i class="fa fa-info-circle"></i> Metadata Field</label>
|
|
103
|
+
<input type="text" id="node-input-metadataField" placeholder="e.g. metadata">
|
|
104
|
+
<div class="form-tips">Optional <code>msg</code> property containing extra metadata (string or object).</div>
|
|
105
|
+
</div>
|
|
106
|
+
<div class="form-row">
|
|
107
|
+
<label for="node-input-contextKey"><i class="fa fa-database"></i> Context Key</label>
|
|
108
|
+
<input type="text" id="node-input-contextKey" placeholder="isa88_batches">
|
|
109
|
+
<div class="form-tips">Global context key used to track active batch records across levels.</div>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="form-row">
|
|
112
|
+
<label> </label>
|
|
113
|
+
<input type="checkbox" id="node-input-endChildren" style="width:auto; margin-right:5px;">
|
|
114
|
+
<label for="node-input-endChildren" style="width:auto;"> Auto-end children when this frame ends</label>
|
|
115
|
+
</div>
|
|
116
|
+
</script>
|
|
117
|
+
|
|
118
|
+
<script type="text/html" data-help-name="event-frame">
|
|
119
|
+
<p>Creates ISA-88 batch structure records with start/end tracking.</p>
|
|
120
|
+
|
|
121
|
+
<h3>ISA-88 Procedural Model</h3>
|
|
122
|
+
<p>The ISA-88 standard defines a hierarchical procedural model:</p>
|
|
123
|
+
<ul>
|
|
124
|
+
<li><b>Procedure</b> — top-level batch recipe</li>
|
|
125
|
+
<li><b>Unit Procedure</b> — sequence within a unit</li>
|
|
126
|
+
<li><b>Operation</b> — major processing step</li>
|
|
127
|
+
<li><b>Phase</b> — lowest-level action</li>
|
|
128
|
+
</ul>
|
|
129
|
+
|
|
130
|
+
<h3>Properties</h3>
|
|
131
|
+
<dl class="message-properties">
|
|
132
|
+
<dt>ISA-88 Level</dt>
|
|
133
|
+
<dd>The procedural level of this batch record.</dd>
|
|
134
|
+
<dt>Parent Level</dt>
|
|
135
|
+
<dd>Optional. Links <code>parent_id</code> to the currently active record at the selected level.
|
|
136
|
+
Not needed when chaining — <code>msg.frame_id</code> from a parent node is used automatically.</dd>
|
|
137
|
+
<dt>Trigger Field</dt>
|
|
138
|
+
<dd>Message property: truthy starts, falsy ends the record.</dd>
|
|
139
|
+
<dt>Batch Name / Batch ID / Unit</dt>
|
|
140
|
+
<dd>Optional fields — can be static strings, msg properties, flow/global context, or env vars.</dd>
|
|
141
|
+
<dt>Metadata Field</dt>
|
|
142
|
+
<dd>Optional msg property for extra metadata (stored as string).</dd>
|
|
143
|
+
<dt>Context Key</dt>
|
|
144
|
+
<dd>Global context key for tracking active records (default: <code>isa88_batches</code>).</dd>
|
|
145
|
+
<dt>Auto-end children</dt>
|
|
146
|
+
<dd>When enabled (default), ending this frame automatically ends all child frames whose <code>parent_id</code> matches this frame's ID. Cascades recursively.</dd>
|
|
147
|
+
</dl>
|
|
148
|
+
|
|
149
|
+
<h3>Inputs</h3>
|
|
150
|
+
<p>Any message. The trigger field determines start/end:</p>
|
|
151
|
+
<ul>
|
|
152
|
+
<li><b>Truthy</b> (and no active record): starts a new batch record</li>
|
|
153
|
+
<li><b>Falsy</b> (and record is active): ends the current record</li>
|
|
154
|
+
</ul>
|
|
155
|
+
|
|
156
|
+
<h3>Outputs</h3>
|
|
157
|
+
<ol>
|
|
158
|
+
<li><b>Start</b> — emits when a new batch record begins</li>
|
|
159
|
+
<li><b>End</b> — emits the completed record with <code>endtime</code> filled in</li>
|
|
160
|
+
</ol>
|
|
161
|
+
|
|
162
|
+
<h3>Output Record</h3>
|
|
163
|
+
<pre>{
|
|
164
|
+
"id": "auto-generated UUID",
|
|
165
|
+
"starttime": "2024-01-15T10:30:00.000Z",
|
|
166
|
+
"endtime": "9999-12-31T23:59:59.000Z",
|
|
167
|
+
"name": "Mixing",
|
|
168
|
+
"parent_id": "parent-uuid-or-empty",
|
|
169
|
+
"level": "operation",
|
|
170
|
+
"state": "complete",
|
|
171
|
+
"batch_id": "BATCH-001",
|
|
172
|
+
"unit": "Reactor-1",
|
|
173
|
+
"metadata": ""
|
|
174
|
+
}</pre>
|
|
175
|
+
|
|
176
|
+
<h3>Chaining</h3>
|
|
177
|
+
<p>Wire the <b>Start</b> output of a parent frame to the input of a child frame.
|
|
178
|
+
The start message carries <code>msg.frame_id</code> which the child automatically
|
|
179
|
+
uses as its <code>parent_id</code>. No need to set Parent Level when chaining.</p>
|
|
180
|
+
<pre>
|
|
181
|
+
[Procedure] ──start──► [Operation] ──start──► [Phase]
|
|
182
|
+
</pre>
|
|
183
|
+
<p>When <b>Auto-end children</b> is enabled, ending the Procedure will automatically
|
|
184
|
+
end the Operation, which in turn ends the Phase (recursive cascade).</p>
|
|
185
|
+
|
|
186
|
+
<h3>Details</h3>
|
|
187
|
+
<p>Use one node per ISA-88 level. Link them by wiring start outputs to child inputs,
|
|
188
|
+
or set <b>Parent Level</b> for lookup-based linking.</p>
|
|
189
|
+
|
|
190
|
+
<p>Multiple instances sharing the same <b>Context Key</b> can coordinate
|
|
191
|
+
across the hierarchy.</p>
|
|
192
|
+
</script>
|