2026-05-10 20:39:54 +02:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
const { BaseDomain, statusBadge } = require('generalFunctions');
|
feat: digital (MQTT) mode + fix silent dispatcher bug for camelCase methods
Runtime:
- Fix silent no-op when user selected any camelCase smoothing or outlier
method from the editor. validateEnum in generalFunctions lowercases enum
values (zScore -> zscore, lowPass -> lowpass, ...) but the dispatcher
compared against camelCase keys. Effect: 5 of 11 smoothing methods
(lowPass, highPass, weightedMovingAverage, bandPass, savitzkyGolay) and
2 of 3 outlier methods (zScore, modifiedZScore) silently fell through.
Users got the raw last value or no outlier filtering with no error log.
Review any pre-2026-04-13 flows that relied on these methods.
Fix: normalize method names to lowercase on both sides of the lookup.
- New Channel class (src/channel.js) — self-contained per-channel pipeline:
outlier -> offset -> scaling -> smoothing -> min/max -> constrain -> emit.
Pure domain logic, no Node-RED deps, reusable by future nodes that need
the same signal-conditioning chain.
Digital mode:
- config.mode.current = 'digital' opts in. config.channels declares one
entry per expected JSON key; each channel has its own type, position,
unit, distance, and optional scaling/smoothing/outlierDetection blocks
that override the top-level analog-mode fields. One MQTT-shaped payload
({t:22.5, h:45, p:1013}) dispatches N independent pipelines and emits N
MeasurementContainer slots from a single input message.
- Backward compatible: absent mode config = analog = pre-digital behaviour.
Every existing measurement flow keeps working unchanged.
UI:
- HTML editor: new Mode dropdown and Channels JSON textarea. The Node-RED
help panel is rewritten end-to-end with topic reference, port contracts,
per-mode configuration, smoothing/outlier method tables, and a note
about the pre-fix behaviour.
- README.md rewritten (was a one-line stub).
Tests (12 -> 71, all green):
- test/basic/smoothing-methods.basic.test.js (+16): every smoothing method
including the formerly-broken camelCase ones.
- test/basic/outlier-detection.basic.test.js (+10): every outlier method,
fall-through, toggle.
- test/basic/scaling-and-interpolation.basic.test.js (+10): offset,
interpolateLinear, constrain, handleScaling edge cases, min/max
tracking, updateOutputPercent fallback, updateOutputAbs emit dedup.
- test/basic/calibration-and-stability.basic.test.js (+11): calibrate
(stable and unstable), isStable, evaluateRepeatability refusals,
toggleSimulation, tick simulation on/off.
- test/integration/digital-mode.integration.test.js (+12): channel build
(including malformed entries), payload dispatch, multi-channel emit,
unknown keys, per-channel scaling/smoothing/outlier, empty channels,
non-numeric value rejection, getDigitalOutput shape, analog-default
back-compat.
E2E verified on Dockerized Node-RED: analog regression unchanged; digital
mode deploys with three channels, dispatches MQTT-style payload, emits
per-channel events, accumulates per-channel smoothing, ignores unknown
keys.
Depends on generalFunctions commit e50be2e (permissive unit check +
mode/channels schema).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 13:43:03 +02:00
|
|
|
const Channel = require('./channel');
|
2026-05-10 20:39:54 +02:00
|
|
|
const Simulator = require('./simulation/simulator');
|
|
|
|
|
const Calibrator = require('./calibration/calibrator');
|
2025-05-14 10:31:50 +02:00
|
|
|
|
2026-05-10 20:39:54 +02:00
|
|
|
// Measurement domain. Analog mode = one Channel built from the flat config.
|
|
|
|
|
// Digital mode = one Channel per config.channels[] entry. Channel owns the
|
|
|
|
|
// outlier → offset → scaling → smoothing → minMax → emit pipeline; the
|
|
|
|
|
// delegates below preserve the pre-refactor public surface for tests.
|
|
|
|
|
class Measurement extends BaseDomain {
|
|
|
|
|
static name = 'measurement';
|
2025-05-14 10:31:50 +02:00
|
|
|
|
2026-05-10 20:39:54 +02:00
|
|
|
configure() {
|
|
|
|
|
this.mode = (this.config?.mode?.current || 'analog').toLowerCase();
|
|
|
|
|
this.channels = new Map();
|
2025-05-14 10:31:50 +02:00
|
|
|
|
2026-05-10 20:39:54 +02:00
|
|
|
if (this.mode === 'digital') {
|
|
|
|
|
this._buildDigitalChannels();
|
|
|
|
|
} else {
|
|
|
|
|
this.analogChannel = this._buildAnalogChannel();
|
|
|
|
|
}
|
2025-05-14 10:31:50 +02:00
|
|
|
|
2026-05-10 20:39:54 +02:00
|
|
|
this._simulator = new Simulator({ config: this.config, logger: this.logger });
|
|
|
|
|
this._calibrator = new Calibrator({
|
|
|
|
|
storedValuesRef: () => this.analogChannel?.storedValues ?? [],
|
|
|
|
|
configRef: () => this.config,
|
|
|
|
|
logger: this.logger,
|
2025-08-07 13:51:28 +02:00
|
|
|
});
|
|
|
|
|
|
2026-05-10 20:39:54 +02:00
|
|
|
this._inputValue = 0;
|
2025-05-14 10:31:50 +02:00
|
|
|
this.simValue = 0;
|
2026-05-10 20:39:54 +02:00
|
|
|
this._installChannelMirrors();
|
2025-05-14 10:31:50 +02:00
|
|
|
|
2026-05-10 20:39:54 +02:00
|
|
|
this.logger.debug(`Measurement id=${this.config.general.id} ready. mode=${this.mode} channels=${this.channels.size}`);
|
|
|
|
|
}
|
feat: digital (MQTT) mode + fix silent dispatcher bug for camelCase methods
Runtime:
- Fix silent no-op when user selected any camelCase smoothing or outlier
method from the editor. validateEnum in generalFunctions lowercases enum
values (zScore -> zscore, lowPass -> lowpass, ...) but the dispatcher
compared against camelCase keys. Effect: 5 of 11 smoothing methods
(lowPass, highPass, weightedMovingAverage, bandPass, savitzkyGolay) and
2 of 3 outlier methods (zScore, modifiedZScore) silently fell through.
Users got the raw last value or no outlier filtering with no error log.
Review any pre-2026-04-13 flows that relied on these methods.
Fix: normalize method names to lowercase on both sides of the lookup.
- New Channel class (src/channel.js) — self-contained per-channel pipeline:
outlier -> offset -> scaling -> smoothing -> min/max -> constrain -> emit.
Pure domain logic, no Node-RED deps, reusable by future nodes that need
the same signal-conditioning chain.
Digital mode:
- config.mode.current = 'digital' opts in. config.channels declares one
entry per expected JSON key; each channel has its own type, position,
unit, distance, and optional scaling/smoothing/outlierDetection blocks
that override the top-level analog-mode fields. One MQTT-shaped payload
({t:22.5, h:45, p:1013}) dispatches N independent pipelines and emits N
MeasurementContainer slots from a single input message.
- Backward compatible: absent mode config = analog = pre-digital behaviour.
Every existing measurement flow keeps working unchanged.
UI:
- HTML editor: new Mode dropdown and Channels JSON textarea. The Node-RED
help panel is rewritten end-to-end with topic reference, port contracts,
per-mode configuration, smoothing/outlier method tables, and a note
about the pre-fix behaviour.
- README.md rewritten (was a one-line stub).
Tests (12 -> 71, all green):
- test/basic/smoothing-methods.basic.test.js (+16): every smoothing method
including the formerly-broken camelCase ones.
- test/basic/outlier-detection.basic.test.js (+10): every outlier method,
fall-through, toggle.
- test/basic/scaling-and-interpolation.basic.test.js (+10): offset,
interpolateLinear, constrain, handleScaling edge cases, min/max
tracking, updateOutputPercent fallback, updateOutputAbs emit dedup.
- test/basic/calibration-and-stability.basic.test.js (+11): calibrate
(stable and unstable), isStable, evaluateRepeatability refusals,
toggleSimulation, tick simulation on/off.
- test/integration/digital-mode.integration.test.js (+12): channel build
(including malformed entries), payload dispatch, multi-channel emit,
unknown keys, per-channel scaling/smoothing/outlier, empty channels,
non-numeric value rejection, getDigitalOutput shape, analog-default
back-compat.
E2E verified on Dockerized Node-RED: analog regression unchanged; digital
mode deploys with three channels, dispatches MQTT-style payload, emits
per-channel events, accumulates per-channel smoothing, ignores unknown
keys.
Depends on generalFunctions commit e50be2e (permissive unit check +
mode/channels schema).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 13:43:03 +02:00
|
|
|
|
2026-05-10 20:39:54 +02:00
|
|
|
// Mirror the analog Channel's state as `m.xxx` so the legacy public surface
|
|
|
|
|
// (outputAbs, storedValues, totalMinValue, …) stays writable from tests.
|
|
|
|
|
_installChannelMirrors() {
|
|
|
|
|
const RW = ['storedValues', 'outputAbs', 'outputPercent', 'totalMinValue',
|
|
|
|
|
'totalMaxValue', 'totalMinSmooth', 'totalMaxSmooth'];
|
|
|
|
|
const RO = ['inputRange', 'processRange'];
|
|
|
|
|
const def = (k, setter) => Object.defineProperty(this, k, {
|
|
|
|
|
configurable: true, enumerable: true,
|
|
|
|
|
get: () => this.analogChannel?.[k] ?? (k === 'storedValues' ? [] : 0),
|
|
|
|
|
...(setter ? { set: setter } : {}),
|
|
|
|
|
});
|
|
|
|
|
for (const k of RW) def(k, (v) => { if (this.analogChannel) this.analogChannel[k] = (k === 'storedValues' && Array.isArray(v)) ? [...v] : v; });
|
|
|
|
|
for (const k of RO) def(k);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_buildAnalogChannel() {
|
|
|
|
|
return new Channel({
|
|
|
|
|
key: null,
|
|
|
|
|
type: this.config.asset.type,
|
|
|
|
|
position: this.config.functionality?.positionVsParent || 'atEquipment',
|
|
|
|
|
unit: this.config.asset?.unit || this.config.general?.unit || 'unitless',
|
|
|
|
|
distance: this.config.functionality?.distance ?? null,
|
|
|
|
|
scaling: this.config.scaling,
|
|
|
|
|
smoothing: this.config.smoothing,
|
|
|
|
|
outlierDetection: this.config.outlierDetection,
|
|
|
|
|
interpolation: this.config.interpolation,
|
|
|
|
|
measurements: this.measurements,
|
|
|
|
|
logger: this.logger,
|
|
|
|
|
});
|
feat: digital (MQTT) mode + fix silent dispatcher bug for camelCase methods
Runtime:
- Fix silent no-op when user selected any camelCase smoothing or outlier
method from the editor. validateEnum in generalFunctions lowercases enum
values (zScore -> zscore, lowPass -> lowpass, ...) but the dispatcher
compared against camelCase keys. Effect: 5 of 11 smoothing methods
(lowPass, highPass, weightedMovingAverage, bandPass, savitzkyGolay) and
2 of 3 outlier methods (zScore, modifiedZScore) silently fell through.
Users got the raw last value or no outlier filtering with no error log.
Review any pre-2026-04-13 flows that relied on these methods.
Fix: normalize method names to lowercase on both sides of the lookup.
- New Channel class (src/channel.js) — self-contained per-channel pipeline:
outlier -> offset -> scaling -> smoothing -> min/max -> constrain -> emit.
Pure domain logic, no Node-RED deps, reusable by future nodes that need
the same signal-conditioning chain.
Digital mode:
- config.mode.current = 'digital' opts in. config.channels declares one
entry per expected JSON key; each channel has its own type, position,
unit, distance, and optional scaling/smoothing/outlierDetection blocks
that override the top-level analog-mode fields. One MQTT-shaped payload
({t:22.5, h:45, p:1013}) dispatches N independent pipelines and emits N
MeasurementContainer slots from a single input message.
- Backward compatible: absent mode config = analog = pre-digital behaviour.
Every existing measurement flow keeps working unchanged.
UI:
- HTML editor: new Mode dropdown and Channels JSON textarea. The Node-RED
help panel is rewritten end-to-end with topic reference, port contracts,
per-mode configuration, smoothing/outlier method tables, and a note
about the pre-fix behaviour.
- README.md rewritten (was a one-line stub).
Tests (12 -> 71, all green):
- test/basic/smoothing-methods.basic.test.js (+16): every smoothing method
including the formerly-broken camelCase ones.
- test/basic/outlier-detection.basic.test.js (+10): every outlier method,
fall-through, toggle.
- test/basic/scaling-and-interpolation.basic.test.js (+10): offset,
interpolateLinear, constrain, handleScaling edge cases, min/max
tracking, updateOutputPercent fallback, updateOutputAbs emit dedup.
- test/basic/calibration-and-stability.basic.test.js (+11): calibrate
(stable and unstable), isStable, evaluateRepeatability refusals,
toggleSimulation, tick simulation on/off.
- test/integration/digital-mode.integration.test.js (+12): channel build
(including malformed entries), payload dispatch, multi-channel emit,
unknown keys, per-channel scaling/smoothing/outlier, empty channels,
non-numeric value rejection, getDigitalOutput shape, analog-default
back-compat.
E2E verified on Dockerized Node-RED: analog regression unchanged; digital
mode deploys with three channels, dispatches MQTT-style payload, emits
per-channel events, accumulates per-channel smoothing, ignores unknown
keys.
Depends on generalFunctions commit e50be2e (permissive unit check +
mode/channels schema).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 13:43:03 +02:00
|
|
|
}
|
2025-05-14 10:31:50 +02:00
|
|
|
|
feat: digital (MQTT) mode + fix silent dispatcher bug for camelCase methods
Runtime:
- Fix silent no-op when user selected any camelCase smoothing or outlier
method from the editor. validateEnum in generalFunctions lowercases enum
values (zScore -> zscore, lowPass -> lowpass, ...) but the dispatcher
compared against camelCase keys. Effect: 5 of 11 smoothing methods
(lowPass, highPass, weightedMovingAverage, bandPass, savitzkyGolay) and
2 of 3 outlier methods (zScore, modifiedZScore) silently fell through.
Users got the raw last value or no outlier filtering with no error log.
Review any pre-2026-04-13 flows that relied on these methods.
Fix: normalize method names to lowercase on both sides of the lookup.
- New Channel class (src/channel.js) — self-contained per-channel pipeline:
outlier -> offset -> scaling -> smoothing -> min/max -> constrain -> emit.
Pure domain logic, no Node-RED deps, reusable by future nodes that need
the same signal-conditioning chain.
Digital mode:
- config.mode.current = 'digital' opts in. config.channels declares one
entry per expected JSON key; each channel has its own type, position,
unit, distance, and optional scaling/smoothing/outlierDetection blocks
that override the top-level analog-mode fields. One MQTT-shaped payload
({t:22.5, h:45, p:1013}) dispatches N independent pipelines and emits N
MeasurementContainer slots from a single input message.
- Backward compatible: absent mode config = analog = pre-digital behaviour.
Every existing measurement flow keeps working unchanged.
UI:
- HTML editor: new Mode dropdown and Channels JSON textarea. The Node-RED
help panel is rewritten end-to-end with topic reference, port contracts,
per-mode configuration, smoothing/outlier method tables, and a note
about the pre-fix behaviour.
- README.md rewritten (was a one-line stub).
Tests (12 -> 71, all green):
- test/basic/smoothing-methods.basic.test.js (+16): every smoothing method
including the formerly-broken camelCase ones.
- test/basic/outlier-detection.basic.test.js (+10): every outlier method,
fall-through, toggle.
- test/basic/scaling-and-interpolation.basic.test.js (+10): offset,
interpolateLinear, constrain, handleScaling edge cases, min/max
tracking, updateOutputPercent fallback, updateOutputAbs emit dedup.
- test/basic/calibration-and-stability.basic.test.js (+11): calibrate
(stable and unstable), isStable, evaluateRepeatability refusals,
toggleSimulation, tick simulation on/off.
- test/integration/digital-mode.integration.test.js (+12): channel build
(including malformed entries), payload dispatch, multi-channel emit,
unknown keys, per-channel scaling/smoothing/outlier, empty channels,
non-numeric value rejection, getDigitalOutput shape, analog-default
back-compat.
E2E verified on Dockerized Node-RED: analog regression unchanged; digital
mode deploys with three channels, dispatches MQTT-style payload, emits
per-channel events, accumulates per-channel smoothing, ignores unknown
keys.
Depends on generalFunctions commit e50be2e (permissive unit check +
mode/channels schema).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 13:43:03 +02:00
|
|
|
_buildDigitalChannels() {
|
|
|
|
|
const entries = Array.isArray(this.config.channels) ? this.config.channels : [];
|
|
|
|
|
if (entries.length === 0) {
|
2026-05-10 20:39:54 +02:00
|
|
|
this.logger.warn('digital mode enabled but config.channels is empty; no channels will be emitted.');
|
feat: digital (MQTT) mode + fix silent dispatcher bug for camelCase methods
Runtime:
- Fix silent no-op when user selected any camelCase smoothing or outlier
method from the editor. validateEnum in generalFunctions lowercases enum
values (zScore -> zscore, lowPass -> lowpass, ...) but the dispatcher
compared against camelCase keys. Effect: 5 of 11 smoothing methods
(lowPass, highPass, weightedMovingAverage, bandPass, savitzkyGolay) and
2 of 3 outlier methods (zScore, modifiedZScore) silently fell through.
Users got the raw last value or no outlier filtering with no error log.
Review any pre-2026-04-13 flows that relied on these methods.
Fix: normalize method names to lowercase on both sides of the lookup.
- New Channel class (src/channel.js) — self-contained per-channel pipeline:
outlier -> offset -> scaling -> smoothing -> min/max -> constrain -> emit.
Pure domain logic, no Node-RED deps, reusable by future nodes that need
the same signal-conditioning chain.
Digital mode:
- config.mode.current = 'digital' opts in. config.channels declares one
entry per expected JSON key; each channel has its own type, position,
unit, distance, and optional scaling/smoothing/outlierDetection blocks
that override the top-level analog-mode fields. One MQTT-shaped payload
({t:22.5, h:45, p:1013}) dispatches N independent pipelines and emits N
MeasurementContainer slots from a single input message.
- Backward compatible: absent mode config = analog = pre-digital behaviour.
Every existing measurement flow keeps working unchanged.
UI:
- HTML editor: new Mode dropdown and Channels JSON textarea. The Node-RED
help panel is rewritten end-to-end with topic reference, port contracts,
per-mode configuration, smoothing/outlier method tables, and a note
about the pre-fix behaviour.
- README.md rewritten (was a one-line stub).
Tests (12 -> 71, all green):
- test/basic/smoothing-methods.basic.test.js (+16): every smoothing method
including the formerly-broken camelCase ones.
- test/basic/outlier-detection.basic.test.js (+10): every outlier method,
fall-through, toggle.
- test/basic/scaling-and-interpolation.basic.test.js (+10): offset,
interpolateLinear, constrain, handleScaling edge cases, min/max
tracking, updateOutputPercent fallback, updateOutputAbs emit dedup.
- test/basic/calibration-and-stability.basic.test.js (+11): calibrate
(stable and unstable), isStable, evaluateRepeatability refusals,
toggleSimulation, tick simulation on/off.
- test/integration/digital-mode.integration.test.js (+12): channel build
(including malformed entries), payload dispatch, multi-channel emit,
unknown keys, per-channel scaling/smoothing/outlier, empty channels,
non-numeric value rejection, getDigitalOutput shape, analog-default
back-compat.
E2E verified on Dockerized Node-RED: analog regression unchanged; digital
mode deploys with three channels, dispatches MQTT-style payload, emits
per-channel events, accumulates per-channel smoothing, ignores unknown
keys.
Depends on generalFunctions commit e50be2e (permissive unit check +
mode/channels schema).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 13:43:03 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
for (const raw of entries) {
|
|
|
|
|
if (!raw || typeof raw !== 'object' || !raw.key || !raw.type) {
|
|
|
|
|
this.logger.warn(`skipping invalid channel entry: ${JSON.stringify(raw)}`);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const channel = new Channel({
|
|
|
|
|
key: raw.key,
|
|
|
|
|
type: raw.type,
|
|
|
|
|
position: raw.position || this.config.functionality?.positionVsParent || 'atEquipment',
|
|
|
|
|
unit: raw.unit || this.config.asset?.unit || 'unitless',
|
|
|
|
|
distance: raw.distance ?? this.config.functionality?.distance ?? null,
|
|
|
|
|
scaling: raw.scaling || { enabled: false, inputMin: 0, inputMax: 1, absMin: 0, absMax: 1, offset: 0 },
|
|
|
|
|
smoothing: raw.smoothing || { smoothWindow: this.config.smoothing.smoothWindow, smoothMethod: this.config.smoothing.smoothMethod },
|
|
|
|
|
outlierDetection: raw.outlierDetection || this.config.outlierDetection,
|
|
|
|
|
interpolation: raw.interpolation || this.config.interpolation,
|
|
|
|
|
measurements: this.measurements,
|
|
|
|
|
logger: this.logger,
|
|
|
|
|
});
|
|
|
|
|
this.channels.set(raw.key, channel);
|
|
|
|
|
}
|
|
|
|
|
this.logger.info(`digital mode: built ${this.channels.size} channel(s) from config.channels`);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 20:39:54 +02:00
|
|
|
// --- digital passthrough ---
|
|
|
|
|
|
feat: digital (MQTT) mode + fix silent dispatcher bug for camelCase methods
Runtime:
- Fix silent no-op when user selected any camelCase smoothing or outlier
method from the editor. validateEnum in generalFunctions lowercases enum
values (zScore -> zscore, lowPass -> lowpass, ...) but the dispatcher
compared against camelCase keys. Effect: 5 of 11 smoothing methods
(lowPass, highPass, weightedMovingAverage, bandPass, savitzkyGolay) and
2 of 3 outlier methods (zScore, modifiedZScore) silently fell through.
Users got the raw last value or no outlier filtering with no error log.
Review any pre-2026-04-13 flows that relied on these methods.
Fix: normalize method names to lowercase on both sides of the lookup.
- New Channel class (src/channel.js) — self-contained per-channel pipeline:
outlier -> offset -> scaling -> smoothing -> min/max -> constrain -> emit.
Pure domain logic, no Node-RED deps, reusable by future nodes that need
the same signal-conditioning chain.
Digital mode:
- config.mode.current = 'digital' opts in. config.channels declares one
entry per expected JSON key; each channel has its own type, position,
unit, distance, and optional scaling/smoothing/outlierDetection blocks
that override the top-level analog-mode fields. One MQTT-shaped payload
({t:22.5, h:45, p:1013}) dispatches N independent pipelines and emits N
MeasurementContainer slots from a single input message.
- Backward compatible: absent mode config = analog = pre-digital behaviour.
Every existing measurement flow keeps working unchanged.
UI:
- HTML editor: new Mode dropdown and Channels JSON textarea. The Node-RED
help panel is rewritten end-to-end with topic reference, port contracts,
per-mode configuration, smoothing/outlier method tables, and a note
about the pre-fix behaviour.
- README.md rewritten (was a one-line stub).
Tests (12 -> 71, all green):
- test/basic/smoothing-methods.basic.test.js (+16): every smoothing method
including the formerly-broken camelCase ones.
- test/basic/outlier-detection.basic.test.js (+10): every outlier method,
fall-through, toggle.
- test/basic/scaling-and-interpolation.basic.test.js (+10): offset,
interpolateLinear, constrain, handleScaling edge cases, min/max
tracking, updateOutputPercent fallback, updateOutputAbs emit dedup.
- test/basic/calibration-and-stability.basic.test.js (+11): calibrate
(stable and unstable), isStable, evaluateRepeatability refusals,
toggleSimulation, tick simulation on/off.
- test/integration/digital-mode.integration.test.js (+12): channel build
(including malformed entries), payload dispatch, multi-channel emit,
unknown keys, per-channel scaling/smoothing/outlier, empty channels,
non-numeric value rejection, getDigitalOutput shape, analog-default
back-compat.
E2E verified on Dockerized Node-RED: analog regression unchanged; digital
mode deploys with three channels, dispatches MQTT-style payload, emits
per-channel events, accumulates per-channel smoothing, ignores unknown
keys.
Depends on generalFunctions commit e50be2e (permissive unit check +
mode/channels schema).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 13:43:03 +02:00
|
|
|
handleDigitalPayload(payload) {
|
|
|
|
|
if (this.mode !== 'digital') {
|
|
|
|
|
this.logger.warn(`handleDigitalPayload called while mode=${this.mode}. Ignoring.`);
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
|
|
|
this.logger.warn(`digital payload must be an object; got ${typeof payload}`);
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
const summary = {};
|
|
|
|
|
const unknown = [];
|
|
|
|
|
for (const [key, raw] of Object.entries(payload)) {
|
|
|
|
|
const channel = this.channels.get(key);
|
2026-05-10 20:39:54 +02:00
|
|
|
if (!channel) { unknown.push(key); continue; }
|
feat: digital (MQTT) mode + fix silent dispatcher bug for camelCase methods
Runtime:
- Fix silent no-op when user selected any camelCase smoothing or outlier
method from the editor. validateEnum in generalFunctions lowercases enum
values (zScore -> zscore, lowPass -> lowpass, ...) but the dispatcher
compared against camelCase keys. Effect: 5 of 11 smoothing methods
(lowPass, highPass, weightedMovingAverage, bandPass, savitzkyGolay) and
2 of 3 outlier methods (zScore, modifiedZScore) silently fell through.
Users got the raw last value or no outlier filtering with no error log.
Review any pre-2026-04-13 flows that relied on these methods.
Fix: normalize method names to lowercase on both sides of the lookup.
- New Channel class (src/channel.js) — self-contained per-channel pipeline:
outlier -> offset -> scaling -> smoothing -> min/max -> constrain -> emit.
Pure domain logic, no Node-RED deps, reusable by future nodes that need
the same signal-conditioning chain.
Digital mode:
- config.mode.current = 'digital' opts in. config.channels declares one
entry per expected JSON key; each channel has its own type, position,
unit, distance, and optional scaling/smoothing/outlierDetection blocks
that override the top-level analog-mode fields. One MQTT-shaped payload
({t:22.5, h:45, p:1013}) dispatches N independent pipelines and emits N
MeasurementContainer slots from a single input message.
- Backward compatible: absent mode config = analog = pre-digital behaviour.
Every existing measurement flow keeps working unchanged.
UI:
- HTML editor: new Mode dropdown and Channels JSON textarea. The Node-RED
help panel is rewritten end-to-end with topic reference, port contracts,
per-mode configuration, smoothing/outlier method tables, and a note
about the pre-fix behaviour.
- README.md rewritten (was a one-line stub).
Tests (12 -> 71, all green):
- test/basic/smoothing-methods.basic.test.js (+16): every smoothing method
including the formerly-broken camelCase ones.
- test/basic/outlier-detection.basic.test.js (+10): every outlier method,
fall-through, toggle.
- test/basic/scaling-and-interpolation.basic.test.js (+10): offset,
interpolateLinear, constrain, handleScaling edge cases, min/max
tracking, updateOutputPercent fallback, updateOutputAbs emit dedup.
- test/basic/calibration-and-stability.basic.test.js (+11): calibrate
(stable and unstable), isStable, evaluateRepeatability refusals,
toggleSimulation, tick simulation on/off.
- test/integration/digital-mode.integration.test.js (+12): channel build
(including malformed entries), payload dispatch, multi-channel emit,
unknown keys, per-channel scaling/smoothing/outlier, empty channels,
non-numeric value rejection, getDigitalOutput shape, analog-default
back-compat.
E2E verified on Dockerized Node-RED: analog regression unchanged; digital
mode deploys with three channels, dispatches MQTT-style payload, emits
per-channel events, accumulates per-channel smoothing, ignores unknown
keys.
Depends on generalFunctions commit e50be2e (permissive unit check +
mode/channels schema).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 13:43:03 +02:00
|
|
|
const v = Number(raw);
|
|
|
|
|
if (!Number.isFinite(v)) {
|
|
|
|
|
this.logger.warn(`digital channel '${key}' received non-numeric value: ${raw}`);
|
|
|
|
|
summary[key] = { ok: false, reason: 'non-numeric' };
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const ok = channel.update(v);
|
|
|
|
|
summary[key] = { ok, mAbs: channel.outputAbs, mPercent: channel.outputPercent };
|
|
|
|
|
}
|
2026-05-10 20:39:54 +02:00
|
|
|
if (unknown.length) this.logger.debug(`digital payload contained unmapped keys: ${unknown.join(', ')}`);
|
feat: digital (MQTT) mode + fix silent dispatcher bug for camelCase methods
Runtime:
- Fix silent no-op when user selected any camelCase smoothing or outlier
method from the editor. validateEnum in generalFunctions lowercases enum
values (zScore -> zscore, lowPass -> lowpass, ...) but the dispatcher
compared against camelCase keys. Effect: 5 of 11 smoothing methods
(lowPass, highPass, weightedMovingAverage, bandPass, savitzkyGolay) and
2 of 3 outlier methods (zScore, modifiedZScore) silently fell through.
Users got the raw last value or no outlier filtering with no error log.
Review any pre-2026-04-13 flows that relied on these methods.
Fix: normalize method names to lowercase on both sides of the lookup.
- New Channel class (src/channel.js) — self-contained per-channel pipeline:
outlier -> offset -> scaling -> smoothing -> min/max -> constrain -> emit.
Pure domain logic, no Node-RED deps, reusable by future nodes that need
the same signal-conditioning chain.
Digital mode:
- config.mode.current = 'digital' opts in. config.channels declares one
entry per expected JSON key; each channel has its own type, position,
unit, distance, and optional scaling/smoothing/outlierDetection blocks
that override the top-level analog-mode fields. One MQTT-shaped payload
({t:22.5, h:45, p:1013}) dispatches N independent pipelines and emits N
MeasurementContainer slots from a single input message.
- Backward compatible: absent mode config = analog = pre-digital behaviour.
Every existing measurement flow keeps working unchanged.
UI:
- HTML editor: new Mode dropdown and Channels JSON textarea. The Node-RED
help panel is rewritten end-to-end with topic reference, port contracts,
per-mode configuration, smoothing/outlier method tables, and a note
about the pre-fix behaviour.
- README.md rewritten (was a one-line stub).
Tests (12 -> 71, all green):
- test/basic/smoothing-methods.basic.test.js (+16): every smoothing method
including the formerly-broken camelCase ones.
- test/basic/outlier-detection.basic.test.js (+10): every outlier method,
fall-through, toggle.
- test/basic/scaling-and-interpolation.basic.test.js (+10): offset,
interpolateLinear, constrain, handleScaling edge cases, min/max
tracking, updateOutputPercent fallback, updateOutputAbs emit dedup.
- test/basic/calibration-and-stability.basic.test.js (+11): calibrate
(stable and unstable), isStable, evaluateRepeatability refusals,
toggleSimulation, tick simulation on/off.
- test/integration/digital-mode.integration.test.js (+12): channel build
(including malformed entries), payload dispatch, multi-channel emit,
unknown keys, per-channel scaling/smoothing/outlier, empty channels,
non-numeric value rejection, getDigitalOutput shape, analog-default
back-compat.
E2E verified on Dockerized Node-RED: analog regression unchanged; digital
mode deploys with three channels, dispatches MQTT-style payload, emits
per-channel events, accumulates per-channel smoothing, ignores unknown
keys.
Depends on generalFunctions commit e50be2e (permissive unit check +
mode/channels schema).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 13:43:03 +02:00
|
|
|
return summary;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getDigitalOutput() {
|
|
|
|
|
const out = { channels: {} };
|
2026-05-10 20:39:54 +02:00
|
|
|
for (const [key, ch] of this.channels) out.channels[key] = ch.getOutput();
|
feat: digital (MQTT) mode + fix silent dispatcher bug for camelCase methods
Runtime:
- Fix silent no-op when user selected any camelCase smoothing or outlier
method from the editor. validateEnum in generalFunctions lowercases enum
values (zScore -> zscore, lowPass -> lowpass, ...) but the dispatcher
compared against camelCase keys. Effect: 5 of 11 smoothing methods
(lowPass, highPass, weightedMovingAverage, bandPass, savitzkyGolay) and
2 of 3 outlier methods (zScore, modifiedZScore) silently fell through.
Users got the raw last value or no outlier filtering with no error log.
Review any pre-2026-04-13 flows that relied on these methods.
Fix: normalize method names to lowercase on both sides of the lookup.
- New Channel class (src/channel.js) — self-contained per-channel pipeline:
outlier -> offset -> scaling -> smoothing -> min/max -> constrain -> emit.
Pure domain logic, no Node-RED deps, reusable by future nodes that need
the same signal-conditioning chain.
Digital mode:
- config.mode.current = 'digital' opts in. config.channels declares one
entry per expected JSON key; each channel has its own type, position,
unit, distance, and optional scaling/smoothing/outlierDetection blocks
that override the top-level analog-mode fields. One MQTT-shaped payload
({t:22.5, h:45, p:1013}) dispatches N independent pipelines and emits N
MeasurementContainer slots from a single input message.
- Backward compatible: absent mode config = analog = pre-digital behaviour.
Every existing measurement flow keeps working unchanged.
UI:
- HTML editor: new Mode dropdown and Channels JSON textarea. The Node-RED
help panel is rewritten end-to-end with topic reference, port contracts,
per-mode configuration, smoothing/outlier method tables, and a note
about the pre-fix behaviour.
- README.md rewritten (was a one-line stub).
Tests (12 -> 71, all green):
- test/basic/smoothing-methods.basic.test.js (+16): every smoothing method
including the formerly-broken camelCase ones.
- test/basic/outlier-detection.basic.test.js (+10): every outlier method,
fall-through, toggle.
- test/basic/scaling-and-interpolation.basic.test.js (+10): offset,
interpolateLinear, constrain, handleScaling edge cases, min/max
tracking, updateOutputPercent fallback, updateOutputAbs emit dedup.
- test/basic/calibration-and-stability.basic.test.js (+11): calibrate
(stable and unstable), isStable, evaluateRepeatability refusals,
toggleSimulation, tick simulation on/off.
- test/integration/digital-mode.integration.test.js (+12): channel build
(including malformed entries), payload dispatch, multi-channel emit,
unknown keys, per-channel scaling/smoothing/outlier, empty channels,
non-numeric value rejection, getDigitalOutput shape, analog-default
back-compat.
E2E verified on Dockerized Node-RED: analog regression unchanged; digital
mode deploys with three channels, dispatches MQTT-style payload, emits
per-channel events, accumulates per-channel smoothing, ignores unknown
keys.
Depends on generalFunctions commit e50be2e (permissive unit check +
mode/channels schema).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 13:43:03 +02:00
|
|
|
return out;
|
2025-05-14 10:31:50 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-10 20:39:54 +02:00
|
|
|
// --- public commands ---
|
2025-05-14 10:31:50 +02:00
|
|
|
|
2026-05-10 20:39:54 +02:00
|
|
|
set inputValue(v) {
|
|
|
|
|
this._inputValue = v;
|
|
|
|
|
if (this.mode === 'analog' && this.analogChannel) {
|
|
|
|
|
this.analogChannel.update(v);
|
|
|
|
|
this.notifyOutputChanged();
|
2025-05-14 10:31:50 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-05-10 20:39:54 +02:00
|
|
|
get inputValue() { return this._inputValue ?? 0; }
|
2025-05-14 10:31:50 +02:00
|
|
|
|
2026-05-10 20:39:54 +02:00
|
|
|
tick() {
|
|
|
|
|
if (this.config?.simulation?.enabled) {
|
|
|
|
|
this.inputValue = this._simulator.step();
|
|
|
|
|
this.simValue = this._simulator.simValue;
|
2025-05-14 10:31:50 +02:00
|
|
|
}
|
2026-05-10 20:39:54 +02:00
|
|
|
return Promise.resolve();
|
2025-05-14 10:31:50 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-10 20:39:54 +02:00
|
|
|
toggleSimulation() {
|
|
|
|
|
this.config.simulation = this.config.simulation || {};
|
|
|
|
|
this.config.simulation.enabled = !this.config.simulation.enabled;
|
2025-05-14 10:31:50 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-10 20:39:54 +02:00
|
|
|
toggleOutlierDetection() {
|
|
|
|
|
this.config.outlierDetection = this.config.outlierDetection || {};
|
|
|
|
|
this.config.outlierDetection.enabled = !Boolean(this.config.outlierDetection.enabled);
|
|
|
|
|
if (this.analogChannel) this.analogChannel.outlierDetection.enabled = this.config.outlierDetection.enabled;
|
2025-05-14 10:31:50 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-10 20:39:54 +02:00
|
|
|
calibrate() {
|
|
|
|
|
const result = this._calibrator.calibrate(this.analogChannel?.outputAbs ?? 0);
|
|
|
|
|
if (result && typeof result.offset === 'number') {
|
|
|
|
|
this.config.scaling.offset = result.offset;
|
|
|
|
|
if (this.analogChannel) this.analogChannel.scaling.offset = result.offset;
|
2025-05-14 10:31:50 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 20:39:54 +02:00
|
|
|
// Legacy shape: <2 samples returns bare `false`; otherwise the
|
|
|
|
|
// {isStable, stdDev} object the calibrator produces.
|
|
|
|
|
isStable() {
|
|
|
|
|
if ((this.storedValues?.length ?? 0) < 2) return false;
|
|
|
|
|
return this._calibrator.isStable();
|
2025-05-14 10:31:50 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-10 20:39:54 +02:00
|
|
|
evaluateRepeatability() {
|
|
|
|
|
const { repeatability } = this._calibrator.evaluateRepeatability();
|
|
|
|
|
return repeatability;
|
2025-05-14 10:31:50 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-10 20:39:54 +02:00
|
|
|
// --- analog pipeline delegates (preserved for tests + back-compat) ---
|
2025-05-14 10:31:50 +02:00
|
|
|
|
|
|
|
|
calculateInput(value) {
|
2026-05-10 20:39:54 +02:00
|
|
|
if (!this.analogChannel) return;
|
|
|
|
|
this.analogChannel.update(value);
|
|
|
|
|
this.notifyOutputChanged();
|
2025-05-14 10:31:50 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-10 20:39:54 +02:00
|
|
|
applyOffset(value) { return value + (this.config.scaling?.offset ?? 0); }
|
|
|
|
|
constrain(v, lo, hi) { return Math.min(Math.max(v, lo), hi); }
|
|
|
|
|
interpolateLinear(n, iMin, iMax, oMin, oMax) {
|
|
|
|
|
if (iMin >= iMax || oMin >= oMax) return n;
|
|
|
|
|
return oMin + ((n - iMin) * (oMax - oMin)) / (iMax - iMin);
|
2025-05-14 10:31:50 +02:00
|
|
|
}
|
2026-05-10 20:39:54 +02:00
|
|
|
handleScaling(value) {
|
|
|
|
|
if (!this.analogChannel) return value;
|
|
|
|
|
const out = this.analogChannel._applyScaling(value);
|
|
|
|
|
// Channel mutates its own scaling copy when inputRange is invalid;
|
|
|
|
|
// mirror that back to config.scaling so the legacy contract holds.
|
|
|
|
|
this.config.scaling.inputMin = this.analogChannel.scaling.inputMin;
|
|
|
|
|
this.config.scaling.inputMax = this.analogChannel.scaling.inputMax;
|
|
|
|
|
return out;
|
2025-05-14 10:31:50 +02:00
|
|
|
}
|
2026-05-10 20:39:54 +02:00
|
|
|
outlierDetection(value) {
|
|
|
|
|
if (!this.analogChannel) return false;
|
|
|
|
|
// Channel skips outlier checks when disabled; the legacy test API expects
|
|
|
|
|
// the check to run regardless of the enabled flag.
|
|
|
|
|
return this.analogChannel._isOutlier(value);
|
2025-05-14 10:31:50 +02:00
|
|
|
}
|
2026-05-10 20:39:54 +02:00
|
|
|
updateOutputPercent(value) { return this.analogChannel?._computePercent(value) ?? 0; }
|
2025-05-14 10:31:50 +02:00
|
|
|
|
2026-05-10 20:39:54 +02:00
|
|
|
// --- output / status ---
|
2025-05-14 10:31:50 +02:00
|
|
|
|
|
|
|
|
getOutput() {
|
2026-05-10 20:39:54 +02:00
|
|
|
if (this.mode === 'digital') return this.getDigitalOutput();
|
2025-05-14 10:31:50 +02:00
|
|
|
return {
|
|
|
|
|
mAbs: this.outputAbs,
|
|
|
|
|
mPercent: this.outputPercent,
|
2026-05-10 20:39:54 +02:00
|
|
|
totalMinValue: this.totalMinValue === Infinity ? 0 : this.totalMinValue,
|
|
|
|
|
totalMaxValue: this.totalMaxValue === -Infinity ? 0 : this.totalMaxValue,
|
2025-05-14 10:31:50 +02:00
|
|
|
totalMinSmooth: this.totalMinSmooth,
|
|
|
|
|
totalMaxSmooth: this.totalMaxSmooth,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 20:39:54 +02:00
|
|
|
getStatusBadge() {
|
|
|
|
|
if (this.mode === 'digital') {
|
|
|
|
|
return statusBadge.compose([`digital · ${this.channels.size} channel(s)`], { fill: 'blue', shape: 'ring' });
|
|
|
|
|
}
|
|
|
|
|
const unit = this.config?.general?.unit || '';
|
|
|
|
|
return statusBadge.compose([`${this.outputAbs} ${unit}`.trim()], { fill: 'green', shape: 'dot' });
|
2025-05-14 10:31:50 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 20:39:54 +02:00
|
|
|
module.exports = Measurement;
|