Lands the end-to-end CoreSync demo (frost-influx-grafana.flow.json) along with the supporting normalizer/identity/interpolation/reducer work, plus a targeted fix for the under-compression bug surfaced by the FROST demo. Under-compression fix (PointReducer): - New burstWindowMs option (default 0, opt-in). When two samples arrive within burstWindowMs of each other, the second is treated as the same wall-clock observation: previous is replaced, slope analysis is skipped. - Without this guard, sub-millisecond bursts (rotatingMachine emits twice per pressure-injection cycle, ~1 ms apart) produced near-vertical apparent slopes that tripped angle-change on every tick — driving cog/efficiency/SEC streams to ~0.6% reduction (i.e. no compression). - With burstWindowMs: 10 in the demo flow, the same streams now compress at 78-93% (verified end-to-end in InfluxDB over a 3-min window). - Editor HTML exposes the new "Burst dt" field with explanatory tooltip. Regression test (test/basic/coresync.basic.test.js): - New "burstWindowMs collapses sub-tick sample bursts into a single observation" test reproduces the exact burst pattern from the demo and asserts before/after behaviour. - Existing 14 tests continue to pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
53 lines
1.8 KiB
JavaScript
53 lines
1.8 KiB
JavaScript
'use strict';
|
|
|
|
const { CoreSyncDomain } = require('./src/coreSyncDomain');
|
|
|
|
module.exports = function(RED) {
|
|
RED.nodes.registerType('coresync', function(config) {
|
|
RED.nodes.createNode(this, config);
|
|
const node = this;
|
|
const hub = new CoreSyncDomain({
|
|
frostBaseUrl: config.frostBaseUrl,
|
|
serviceVersion: config.serviceVersion,
|
|
dbaseFormat: config.dbaseFormat,
|
|
assetTagOverride: config.assetTagOverride,
|
|
sensorTagOverride: config.sensorTagOverride,
|
|
maxQueuedObservationsPerStream: config.maxQueuedObservationsPerStream,
|
|
reducer: {
|
|
angleToleranceDeg: Number(config.angleToleranceDeg),
|
|
timeScaleMs: Number(config.timeScaleMs),
|
|
maxGapMs: Number(config.maxGapMs),
|
|
minDeltaTimeMs: Number(config.minDeltaTimeMs),
|
|
minDeltaValue: Number(config.minDeltaValue),
|
|
burstWindowMs: Number(config.burstWindowMs) || 0,
|
|
comparisonMode: config.comparisonMode || 'angle',
|
|
},
|
|
});
|
|
|
|
node.on('input', (msg, send, done) => {
|
|
try {
|
|
const output = hub.handleMessage(msg);
|
|
const dbaseCount = Array.isArray(output[1]) ? output[1].length : (output[1] ? 1 : 0);
|
|
if (dbaseCount > 0) node.status({ fill: 'blue', shape: 'dot', text: `${dbaseCount} FROST request(s)` });
|
|
send(output);
|
|
} catch (error) {
|
|
node.status({ fill: 'red', shape: 'ring', text: error.message });
|
|
node.error(error, msg);
|
|
} finally {
|
|
if (typeof done === 'function') done();
|
|
}
|
|
});
|
|
|
|
node.on('close', (removed, done) => {
|
|
const closeDone = typeof removed === 'function' ? removed : done;
|
|
try {
|
|
const output = hub.flushAll('close');
|
|
if (output[1]) node.send(output);
|
|
node.status({});
|
|
} finally {
|
|
if (typeof closeDone === 'function') closeDone();
|
|
}
|
|
});
|
|
});
|
|
};
|