Files
coresync/coresync.html
znetsixe 21d77a8afa feat(coresync): FROST/Influx/Grafana demo + sub-tick burst-window reducer fix
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>
2026-05-22 20:27:28 +02:00

101 lines
4.9 KiB
HTML

<script type="text/javascript">
RED.nodes.registerType('coresync', {
category: 'EVOLV',
color: '#54647B',
defaults: {
name: { value: '' },
frostBaseUrl: { value: 'http://localhost:8080/FROST-Server', required: true },
serviceVersion: { value: 'v1.1', required: true },
dbaseFormat: { value: 'frost' },
assetTagOverride: { value: '' },
sensorTagOverride: { value: '' },
comparisonMode: { value: 'angle' },
angleToleranceDeg: { value: 5, required: true, validate: RED.validators.number() },
timeScaleMs: { value: 60000, required: true, validate: RED.validators.number() },
maxGapMs: { value: 300000, required: true, validate: RED.validators.number() },
minDeltaTimeMs: { value: 0, required: true, validate: RED.validators.number() },
minDeltaValue: { value: 0, required: true, validate: RED.validators.number() },
burstWindowMs: { value: 0, required: true, validate: RED.validators.number() },
maxQueuedObservationsPerStream: { value: 2, required: true, validate: RED.validators.number() },
diagnosticsEnabled: { value: false },
},
inputs: 1,
outputs: 3,
inputLabels: ['EVOLV dbase / FROST response'],
outputLabels: ['process', 'dbase', 'parent'],
icon: 'font-awesome/fa-cloud-upload',
label: function() {
return this.name || 'CoreSync';
},
});
</script>
<script type="text/html" data-template-name="coresync">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name">
</div>
<div class="form-row">
<label for="node-input-frostBaseUrl"><i class="fa fa-globe"></i> FROST URL</label>
<input type="text" id="node-input-frostBaseUrl">
</div>
<div class="form-row">
<label for="node-input-serviceVersion"><i class="fa fa-code-fork"></i> Version</label>
<input type="text" id="node-input-serviceVersion">
</div>
<div class="form-row">
<label for="node-input-dbaseFormat"><i class="fa fa-database"></i> Output</label>
<select id="node-input-dbaseFormat" style="width:70%;">
<option value="frost">frost</option>
</select>
</div>
<div class="form-row">
<label for="node-input-assetTagOverride"><i class="fa fa-cube"></i> Thing Tag</label>
<input type="text" id="node-input-assetTagOverride" placeholder="optional override">
</div>
<div class="form-row">
<label for="node-input-sensorTagOverride"><i class="fa fa-bullseye"></i> Sensor Tag</label>
<input type="text" id="node-input-sensorTagOverride" placeholder="optional override">
</div>
<div class="form-row">
<label for="node-input-comparisonMode"><i class="fa fa-compress"></i> Reducer</label>
<select id="node-input-comparisonMode" style="width:70%;">
<option value="angle">Angle change</option>
<option value="relative-slope">Relative slope change</option>
</select>
</div>
<div class="form-row">
<label for="node-input-angleToleranceDeg"><i class="fa fa-arrows-h"></i> Angle Tol.</label>
<input type="number" id="node-input-angleToleranceDeg" style="width:70%;">
</div>
<div class="form-row">
<label for="node-input-timeScaleMs"><i class="fa fa-clock-o"></i> Time Scale</label>
<input type="number" id="node-input-timeScaleMs" style="width:70%;">
</div>
<div class="form-row">
<label for="node-input-maxGapMs"><i class="fa fa-hourglass-half"></i> Max Gap</label>
<input type="number" id="node-input-maxGapMs" style="width:70%;">
</div>
<div class="form-row">
<label for="node-input-minDeltaTimeMs"><i class="fa fa-filter"></i> Min dt</label>
<input type="number" id="node-input-minDeltaTimeMs" style="width:70%;">
</div>
<div class="form-row">
<label for="node-input-minDeltaValue"><i class="fa fa-filter"></i> Min dv</label>
<input type="number" id="node-input-minDeltaValue" style="width:70%;">
</div>
<div class="form-row">
<label for="node-input-burstWindowMs" title="Collapse sub-tick sample bursts: when two samples arrive within this many milliseconds, treat them as the same observation. 0 disables. Set to ~10 ms when an upstream node emits multiple telemetry rows per logical tick (e.g. rotatingMachine reacting to two simultaneous simulateMeasurement msgs)."><i class="fa fa-compress"></i> Burst dt</label>
<input type="number" id="node-input-burstWindowMs" style="width:70%;">
</div>
<div class="form-row">
<label for="node-input-maxQueuedObservationsPerStream"><i class="fa fa-list"></i> Queue</label>
<input type="number" id="node-input-maxQueuedObservationsPerStream" style="width:70%;">
</div>
</script>
<script type="text/html" data-help-name="coresync">
<p>Collects EVOLV dbase telemetry, reduces numeric streams, and emits FROST-ready HTTP request messages on the dbase output.</p>
<p>Feed HTTP responses back into this node with <code>msg.topic = "frost.response"</code> so metadata lookup/create state can advance.</p>
</script>