Files
measurement/src/specificClass.js

239 lines
8.9 KiB
JavaScript
Raw Normal View History

'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');
const Simulator = require('./simulation/simulator');
const Calibrator = require('./calibration/calibrator');
2025-05-14 10:31:50 +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
configure() {
this.mode = (this.config?.mode?.current || 'analog').toLowerCase();
this.channels = new Map();
2025-05-14 10:31:50 +02:00
if (this.mode === 'digital') {
this._buildDigitalChannels();
} else {
this.analogChannel = this._buildAnalogChannel();
}
2025-05-14 10:31:50 +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,
});
this._inputValue = 0;
2025-05-14 10:31:50 +02:00
this.simValue = 0;
this._installChannelMirrors();
2025-05-14 10:31:50 +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
// 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) {
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`);
}
// --- 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);
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 };
}
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: {} };
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
}
// --- public commands ---
2025-05-14 10:31:50 +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
}
}
get inputValue() { return this._inputValue ?? 0; }
2025-05-14 10:31:50 +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
}
return Promise.resolve();
2025-05-14 10:31:50 +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
}
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
}
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
}
}
// 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
}
evaluateRepeatability() {
const { repeatability } = this._calibrator.evaluateRepeatability();
return repeatability;
2025-05-14 10:31:50 +02:00
}
// --- analog pipeline delegates (preserved for tests + back-compat) ---
2025-05-14 10:31:50 +02:00
calculateInput(value) {
if (!this.analogChannel) return;
this.analogChannel.update(value);
this.notifyOutputChanged();
2025-05-14 10:31:50 +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
}
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
}
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
}
updateOutputPercent(value) { return this.analogChannel?._computePercent(value) ?? 0; }
2025-05-14 10:31:50 +02:00
// --- output / status ---
2025-05-14 10:31:50 +02:00
getOutput() {
if (this.mode === 'digital') return this.getDigitalOutput();
2025-05-14 10:31:50 +02:00
return {
mAbs: this.outputAbs,
mPercent: this.outputPercent,
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,
};
}
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
}
}
module.exports = Measurement;