2025-10-14 13:52:34 +02:00
<!--
| S88-niveau | Primair (blokkleur) | Tekstkleur |
| ---------------------- | ------------------- | ---------- |
| **Area** | `#0f52a5` | wit |
| **Process Cell** | `#0c99d9` | wit |
| **Unit** | `#50a8d9` | zwart |
| **Equipment (Module)** | `#86bbdd` | zwart |
| **Control Module** | `#a9daee` | zwart |
2025-05-14 10:31:50 +02:00
2025-10-14 13:52:34 +02:00
-->
2025-06-23 13:23:51 +02:00
< script src = "/measurement/menu.js" > < / script > <!-- Load the menu script for dynamic dropdowns -->
< script src = "/measurement/configData.js" > < / script > <!-- Load the config script for node information -->
2025-06-20 17:14:22 +02:00
2025-06-12 17:05:28 +02:00
< script >
2025-05-14 10:31:50 +02:00
RED.nodes.registerType("measurement", {
2025-06-25 17:25:13 +02:00
category: "EVOLV",
2025-10-14 13:52:34 +02:00
color: "#a9daee", // color for the node based on the S88 schema
2025-05-14 10:31:50 +02:00
defaults: {
// Define default properties
2025-11-13 19:38:25 +01:00
name: { value: "" }, // use asset category as name
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
// Input mode: 'analog' (scalar payload, default) or 'digital' (object payload, many channels)
mode: { value: "analog" },
channels: { value: "[]" },
// Define specific properties (analog-mode pipeline defaults)
2025-05-14 10:31:50 +02:00
scaling: { value: false },
i_min: { value: 0, required: true },
i_max: { value: 0, required: true },
i_offset: { value: 0 },
o_min: { value: 0, required: true },
o_max: { value: 1, required: true },
simulator: { value: false },
smooth_method: { value: "" },
count: { value: "10", required: true },
2026-03-12 16:39:25 +01:00
processOutputFormat: { value: "process" },
dbaseOutputFormat: { value: "influxdb" },
2025-05-14 10:31:50 +02:00
//define asset properties
2025-06-25 14:52:20 +02:00
uuid: { value: "" },
2025-05-14 10:31:50 +02:00
supplier: { value: "" },
2025-06-20 17:14:22 +02:00
category: { value: "" },
assetType: { value: "" },
2025-05-14 10:31:50 +02:00
model: { value: "" },
unit: { value: "" },
2026-01-29 09:16:33 +01:00
assetTagNumber: { value: "" },
2025-05-14 10:31:50 +02:00
2025-06-25 10:43:15 +02:00
//logger properties
enableLog: { value: false },
logLevel: { value: "error" },
//physicalAspect
2025-06-25 11:45:32 +02:00
positionVsParent: { value: "" },
2025-07-01 15:24:18 +02:00
positionIcon: { value: "" },
2025-09-05 16:20:12 +02:00
hasDistance: { value: false },
distance: { value: 0 },
distanceUnit: { value: "m" },
distanceDescription: { value: "" }
2025-06-25 10:43:15 +02:00
2025-05-14 10:31:50 +02:00
},
inputs: 1,
2025-06-20 17:14:22 +02:00
outputs: 3,
2025-06-25 17:25:13 +02:00
inputLabels: ["Input"],
2025-06-20 17:14:22 +02:00
outputLabels: ["process", "dbase", "parent"],
2025-10-14 13:52:34 +02:00
icon: "font-awesome/fa-sliders",
2025-05-14 10:31:50 +02:00
label: function () {
fix(editor): make Input Mode the top-level switch, hide wrong-mode fields
Prior behaviour: the Mode dropdown existed but nothing consumed it in the
editor — analog fields (Scaling, Source Min/Max, Smoothing, …) were
always visible, and the Channels JSON editor was always visible too.
For a legacy node with no saved mode the dropdown defaulted blank so
users reported "I cant even select digital or analog".
Changes:
- Initialize the Mode <select> from node.mode with an 'analog' fallback
for legacy nodes (safe default — matches pre-digital behaviour).
- Wrap analog-only fields and digital-only fields in labelled containers
and toggle their display based on the selected mode. Mode change is
live — no redeploy needed to see the right form.
- Inline hint under the Mode dropdown tells the user what payload shape
is expected for the current mode.
- Channels JSON gets live validation — shows channel count + names on
valid JSON, warns on missing key/type, errors on invalid JSON.
- Label function appends ' [digital]' so the node visibly differs in a
flow from an analog sibling.
- oneditsave is mode-aware: only warns about incomplete scaling ranges
in analog mode; in digital mode warns if the channels array is empty
or unparseable.
Runtime friendliness:
- nodeClass node-status now shows 'digital · N channel(s)' on startup in
digital mode, and 'digital · N/M ch updated' after each incoming msg
so the editor has a live heartbeat even when there is no single scalar.
- When analog mode receives an object payload (or digital receives a
number), the node logs an actionable warn suggesting the mode switch
instead of silently dropping the message.
Explicit, not auto-detected: mode remains a deployment-time choice
because the two modes take different editor config (scaling/smoothing vs
channels map). Auto-detecting at runtime would leave the node
unconfigured in whichever mode the user hadn't anticipated.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:00:34 +02:00
const modeTag = this.mode === 'digital' ? ' [digital]' : '';
return (this.positionIcon || "") + " " + (this.assetType || "Measurement") + modeTag;
2025-05-14 10:31:50 +02:00
},
2025-06-20 17:14:22 +02:00
2025-05-14 10:31:50 +02:00
oneditprepare: function() {
fix(editor): make Input Mode the top-level switch, hide wrong-mode fields
Prior behaviour: the Mode dropdown existed but nothing consumed it in the
editor — analog fields (Scaling, Source Min/Max, Smoothing, …) were
always visible, and the Channels JSON editor was always visible too.
For a legacy node with no saved mode the dropdown defaulted blank so
users reported "I cant even select digital or analog".
Changes:
- Initialize the Mode <select> from node.mode with an 'analog' fallback
for legacy nodes (safe default — matches pre-digital behaviour).
- Wrap analog-only fields and digital-only fields in labelled containers
and toggle their display based on the selected mode. Mode change is
live — no redeploy needed to see the right form.
- Inline hint under the Mode dropdown tells the user what payload shape
is expected for the current mode.
- Channels JSON gets live validation — shows channel count + names on
valid JSON, warns on missing key/type, errors on invalid JSON.
- Label function appends ' [digital]' so the node visibly differs in a
flow from an analog sibling.
- oneditsave is mode-aware: only warns about incomplete scaling ranges
in analog mode; in digital mode warns if the channels array is empty
or unparseable.
Runtime friendliness:
- nodeClass node-status now shows 'digital · N channel(s)' on startup in
digital mode, and 'digital · N/M ch updated' after each incoming msg
so the editor has a live heartbeat even when there is no single scalar.
- When analog mode receives an object payload (or digital receives a
number), the node logs an actionable warn suggesting the mode switch
instead of silently dropping the message.
Explicit, not auto-detected: mode remains a deployment-time choice
because the two modes take different editor config (scaling/smoothing vs
channels map). Auto-detecting at runtime would leave the node
unconfigured in whichever mode the user hadn't anticipated.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:00:34 +02:00
const node = this;
// === Mode selector — TOP-LEVEL hierarchy ===
// The Input Mode drives whether analog-pipeline fields or the
// digital channels editor are shown. Initialize the < select > from
// the saved node value, fall back to 'analog' for legacy nodes
// that were saved before the mode field existed.
const modeSelect = document.getElementById('node-input-mode');
const initialMode = (node.mode === 'digital' || node.mode === 'analog') ? node.mode : 'analog';
modeSelect.value = initialMode;
const analogBlock = document.getElementById('analog-only-fields');
const digitalBlock = document.getElementById('digital-only-fields');
const modeHint = document.getElementById('mode-hint');
function applyMode(mode) {
const isDigital = mode === 'digital';
analogBlock.style.display = isDigital ? 'none' : 'block';
digitalBlock.style.display = isDigital ? 'block' : 'none';
if (modeHint) {
modeHint.textContent = isDigital
? 'msg.payload must be an OBJECT, e.g. {"temperature": 22.5, "humidity": 45}. Define each key below.'
: 'msg.payload must be a NUMBER (or numeric string). Configure scaling/smoothing below.';
}
validateChannelsJson();
}
modeSelect.addEventListener('change', (e) => applyMode(e.target.value));
applyMode(initialMode);
// === Channels JSON live validation (digital only) ===
const channelsArea = document.getElementById('node-input-channels');
const channelsHint = document.getElementById('channels-validation');
if (channelsArea & & typeof node.channels === 'string') {
channelsArea.value = node.channels;
}
function validateChannelsJson() {
if (!channelsHint) return;
if (modeSelect.value !== 'digital') { channelsHint.textContent = ''; return; }
const raw = (channelsArea.value || '').trim();
if (!raw || raw === '[]') {
channelsHint.innerHTML = '< span style = "color:#b45309;" > Digital mode with no channels — no measurements will be emitted.< / span > ';
return;
}
try {
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) throw new Error('must be an array');
const missing = parsed
.map((c, i) => (c & & c.key & & c.type ? null : `entry ${i}: missing key or type`))
.filter(Boolean);
if (missing.length) {
channelsHint.innerHTML = '< span style = "color:#b45309;" > ' + missing.join('; ') + '< / span > ';
} else {
channelsHint.innerHTML = '< span style = "color:#047857;" > ' + parsed.length + ' channel(s) defined: ' + parsed.map((c) => c.key).join(', ') + '< / span > ';
}
} catch (e) {
channelsHint.innerHTML = '< span style = "color:#b91c1c;" > Invalid JSON: ' + e.message + '< / span > ';
}
}
if (channelsArea) channelsArea.addEventListener('input', validateChannelsJson);
// === Asset / logger / position placeholders (dynamic menus) ===
2025-06-20 17:14:22 +02:00
const waitForMenuData = () => {
if (window.EVOLV?.nodes?.measurement?.initEditor) {
fix(editor): make Input Mode the top-level switch, hide wrong-mode fields
Prior behaviour: the Mode dropdown existed but nothing consumed it in the
editor — analog fields (Scaling, Source Min/Max, Smoothing, …) were
always visible, and the Channels JSON editor was always visible too.
For a legacy node with no saved mode the dropdown defaulted blank so
users reported "I cant even select digital or analog".
Changes:
- Initialize the Mode <select> from node.mode with an 'analog' fallback
for legacy nodes (safe default — matches pre-digital behaviour).
- Wrap analog-only fields and digital-only fields in labelled containers
and toggle their display based on the selected mode. Mode change is
live — no redeploy needed to see the right form.
- Inline hint under the Mode dropdown tells the user what payload shape
is expected for the current mode.
- Channels JSON gets live validation — shows channel count + names on
valid JSON, warns on missing key/type, errors on invalid JSON.
- Label function appends ' [digital]' so the node visibly differs in a
flow from an analog sibling.
- oneditsave is mode-aware: only warns about incomplete scaling ranges
in analog mode; in digital mode warns if the channels array is empty
or unparseable.
Runtime friendliness:
- nodeClass node-status now shows 'digital · N channel(s)' on startup in
digital mode, and 'digital · N/M ch updated' after each incoming msg
so the editor has a live heartbeat even when there is no single scalar.
- When analog mode receives an object payload (or digital receives a
number), the node logs an actionable warn suggesting the mode switch
instead of silently dropping the message.
Explicit, not auto-detected: mode remains a deployment-time choice
because the two modes take different editor config (scaling/smoothing vs
channels map). Auto-detecting at runtime would leave the node
unconfigured in whichever mode the user hadn't anticipated.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:00:34 +02:00
window.EVOLV.nodes.measurement.initEditor(node);
2025-06-20 17:14:22 +02:00
} else {
setTimeout(waitForMenuData, 50);
}
};
waitForMenuData();
fix(editor): make Input Mode the top-level switch, hide wrong-mode fields
Prior behaviour: the Mode dropdown existed but nothing consumed it in the
editor — analog fields (Scaling, Source Min/Max, Smoothing, …) were
always visible, and the Channels JSON editor was always visible too.
For a legacy node with no saved mode the dropdown defaulted blank so
users reported "I cant even select digital or analog".
Changes:
- Initialize the Mode <select> from node.mode with an 'analog' fallback
for legacy nodes (safe default — matches pre-digital behaviour).
- Wrap analog-only fields and digital-only fields in labelled containers
and toggle their display based on the selected mode. Mode change is
live — no redeploy needed to see the right form.
- Inline hint under the Mode dropdown tells the user what payload shape
is expected for the current mode.
- Channels JSON gets live validation — shows channel count + names on
valid JSON, warns on missing key/type, errors on invalid JSON.
- Label function appends ' [digital]' so the node visibly differs in a
flow from an analog sibling.
- oneditsave is mode-aware: only warns about incomplete scaling ranges
in analog mode; in digital mode warns if the channels array is empty
or unparseable.
Runtime friendliness:
- nodeClass node-status now shows 'digital · N channel(s)' on startup in
digital mode, and 'digital · N/M ch updated' after each incoming msg
so the editor has a live heartbeat even when there is no single scalar.
- When analog mode receives an object payload (or digital receives a
number), the node logs an actionable warn suggesting the mode switch
instead of silently dropping the message.
Explicit, not auto-detected: mode remains a deployment-time choice
because the two modes take different editor config (scaling/smoothing vs
channels map). Auto-detecting at runtime would leave the node
unconfigured in whichever mode the user hadn't anticipated.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:00:34 +02:00
// === Smoothing method dropdown (analog only) ===
2025-06-24 10:48:40 +02:00
const smoothMethodSelect = document.getElementById('node-input-smooth_method');
const options = window.EVOLV?.nodes?.measurement?.config?.smoothing?.smoothMethod?.rules?.values || [];
smoothMethodSelect.innerHTML = '';
const emptyOption = document.createElement('option');
emptyOption.value = '';
emptyOption.textContent = 'Select method...';
smoothMethodSelect.appendChild(emptyOption);
options.forEach(option => {
fix(editor): make Input Mode the top-level switch, hide wrong-mode fields
Prior behaviour: the Mode dropdown existed but nothing consumed it in the
editor — analog fields (Scaling, Source Min/Max, Smoothing, …) were
always visible, and the Channels JSON editor was always visible too.
For a legacy node with no saved mode the dropdown defaulted blank so
users reported "I cant even select digital or analog".
Changes:
- Initialize the Mode <select> from node.mode with an 'analog' fallback
for legacy nodes (safe default — matches pre-digital behaviour).
- Wrap analog-only fields and digital-only fields in labelled containers
and toggle their display based on the selected mode. Mode change is
live — no redeploy needed to see the right form.
- Inline hint under the Mode dropdown tells the user what payload shape
is expected for the current mode.
- Channels JSON gets live validation — shows channel count + names on
valid JSON, warns on missing key/type, errors on invalid JSON.
- Label function appends ' [digital]' so the node visibly differs in a
flow from an analog sibling.
- oneditsave is mode-aware: only warns about incomplete scaling ranges
in analog mode; in digital mode warns if the channels array is empty
or unparseable.
Runtime friendliness:
- nodeClass node-status now shows 'digital · N channel(s)' on startup in
digital mode, and 'digital · N/M ch updated' after each incoming msg
so the editor has a live heartbeat even when there is no single scalar.
- When analog mode receives an object payload (or digital receives a
number), the node logs an actionable warn suggesting the mode switch
instead of silently dropping the message.
Explicit, not auto-detected: mode remains a deployment-time choice
because the two modes take different editor config (scaling/smoothing vs
channels map). Auto-detecting at runtime would leave the node
unconfigured in whichever mode the user hadn't anticipated.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:00:34 +02:00
const optionElement = document.createElement('option');
optionElement.value = option.value;
optionElement.textContent = option.value;
optionElement.title = option.description;
smoothMethodSelect.appendChild(optionElement);
2025-06-24 10:48:40 +02:00
});
fix(editor): make Input Mode the top-level switch, hide wrong-mode fields
Prior behaviour: the Mode dropdown existed but nothing consumed it in the
editor — analog fields (Scaling, Source Min/Max, Smoothing, …) were
always visible, and the Channels JSON editor was always visible too.
For a legacy node with no saved mode the dropdown defaulted blank so
users reported "I cant even select digital or analog".
Changes:
- Initialize the Mode <select> from node.mode with an 'analog' fallback
for legacy nodes (safe default — matches pre-digital behaviour).
- Wrap analog-only fields and digital-only fields in labelled containers
and toggle their display based on the selected mode. Mode change is
live — no redeploy needed to see the right form.
- Inline hint under the Mode dropdown tells the user what payload shape
is expected for the current mode.
- Channels JSON gets live validation — shows channel count + names on
valid JSON, warns on missing key/type, errors on invalid JSON.
- Label function appends ' [digital]' so the node visibly differs in a
flow from an analog sibling.
- oneditsave is mode-aware: only warns about incomplete scaling ranges
in analog mode; in digital mode warns if the channels array is empty
or unparseable.
Runtime friendliness:
- nodeClass node-status now shows 'digital · N channel(s)' on startup in
digital mode, and 'digital · N/M ch updated' after each incoming msg
so the editor has a live heartbeat even when there is no single scalar.
- When analog mode receives an object payload (or digital receives a
number), the node logs an actionable warn suggesting the mode switch
instead of silently dropping the message.
Explicit, not auto-detected: mode remains a deployment-time choice
because the two modes take different editor config (scaling/smoothing vs
channels map). Auto-detecting at runtime would leave the node
unconfigured in whichever mode the user hadn't anticipated.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:00:34 +02:00
if (node.smooth_method) smoothMethodSelect.value = node.smooth_method;
// === Scale rows toggle (analog only) ===
const chk = document.getElementById('node-input-scaling');
const rowMin = document.getElementById('row-input-i_min');
const rowMax = document.getElementById('row-input-i_max');
function toggleScalingRows() {
const show = chk.checked;
rowMin.style.display = show ? 'block' : 'none';
rowMax.style.display = show ? 'block' : 'none';
2025-06-24 10:48:40 +02:00
}
fix(editor): make Input Mode the top-level switch, hide wrong-mode fields
Prior behaviour: the Mode dropdown existed but nothing consumed it in the
editor — analog fields (Scaling, Source Min/Max, Smoothing, …) were
always visible, and the Channels JSON editor was always visible too.
For a legacy node with no saved mode the dropdown defaulted blank so
users reported "I cant even select digital or analog".
Changes:
- Initialize the Mode <select> from node.mode with an 'analog' fallback
for legacy nodes (safe default — matches pre-digital behaviour).
- Wrap analog-only fields and digital-only fields in labelled containers
and toggle their display based on the selected mode. Mode change is
live — no redeploy needed to see the right form.
- Inline hint under the Mode dropdown tells the user what payload shape
is expected for the current mode.
- Channels JSON gets live validation — shows channel count + names on
valid JSON, warns on missing key/type, errors on invalid JSON.
- Label function appends ' [digital]' so the node visibly differs in a
flow from an analog sibling.
- oneditsave is mode-aware: only warns about incomplete scaling ranges
in analog mode; in digital mode warns if the channels array is empty
or unparseable.
Runtime friendliness:
- nodeClass node-status now shows 'digital · N channel(s)' on startup in
digital mode, and 'digital · N/M ch updated' after each incoming msg
so the editor has a live heartbeat even when there is no single scalar.
- When analog mode receives an object payload (or digital receives a
number), the node logs an actionable warn suggesting the mode switch
instead of silently dropping the message.
Explicit, not auto-detected: mode remains a deployment-time choice
because the two modes take different editor config (scaling/smoothing vs
channels map). Auto-detecting at runtime would leave the node
unconfigured in whichever mode the user hadn't anticipated.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:00:34 +02:00
chk.addEventListener('change', toggleScalingRows);
toggleScalingRows();
2025-06-25 14:52:20 +02:00
2025-06-24 10:48:40 +02:00
//------------------- END OF CUSTOM config UI ELEMENTS ------------------- //
2025-06-20 17:14:22 +02:00
},
2025-05-14 10:31:50 +02:00
oneditsave: function () {
const node = this;
2025-06-24 10:48:40 +02:00
// Validate asset properties using the asset menu
if (window.EVOLV?.nodes?.measurement?.assetMenu?.saveEditor) {
success = window.EVOLV.nodes.measurement.assetMenu.saveEditor(this);
}
2025-05-14 10:31:50 +02:00
2025-06-25 10:43:15 +02:00
// Validate logger properties using the logger menu
if (window.EVOLV?.nodes?.measurement?.loggerMenu?.saveEditor) {
success = window.EVOLV.nodes.measurement.loggerMenu.saveEditor(node);
}
2025-07-01 15:24:18 +02:00
// save position field
2025-07-01 17:02:17 +02:00
if (window.EVOLV?.nodes?.measurement?.positionMenu?.saveEditor) {
2025-07-24 13:15:56 +02:00
window.EVOLV.nodes.measurement.positionMenu.saveEditor(this);
2025-07-01 15:24:18 +02:00
}
fix(editor): make Input Mode the top-level switch, hide wrong-mode fields
Prior behaviour: the Mode dropdown existed but nothing consumed it in the
editor — analog fields (Scaling, Source Min/Max, Smoothing, …) were
always visible, and the Channels JSON editor was always visible too.
For a legacy node with no saved mode the dropdown defaulted blank so
users reported "I cant even select digital or analog".
Changes:
- Initialize the Mode <select> from node.mode with an 'analog' fallback
for legacy nodes (safe default — matches pre-digital behaviour).
- Wrap analog-only fields and digital-only fields in labelled containers
and toggle their display based on the selected mode. Mode change is
live — no redeploy needed to see the right form.
- Inline hint under the Mode dropdown tells the user what payload shape
is expected for the current mode.
- Channels JSON gets live validation — shows channel count + names on
valid JSON, warns on missing key/type, errors on invalid JSON.
- Label function appends ' [digital]' so the node visibly differs in a
flow from an analog sibling.
- oneditsave is mode-aware: only warns about incomplete scaling ranges
in analog mode; in digital mode warns if the channels array is empty
or unparseable.
Runtime friendliness:
- nodeClass node-status now shows 'digital · N channel(s)' on startup in
digital mode, and 'digital · N/M ch updated' after each incoming msg
so the editor has a live heartbeat even when there is no single scalar.
- When analog mode receives an object payload (or digital receives a
number), the node logs an actionable warn suggesting the mode switch
instead of silently dropping the message.
Explicit, not auto-detected: mode remains a deployment-time choice
because the two modes take different editor config (scaling/smoothing vs
channels map). Auto-detecting at runtime would leave the node
unconfigured in whichever mode the user hadn't anticipated.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:00:34 +02:00
// Mode is the top-level switch. Always save it first; its value
// drives which other fields are meaningful.
node.mode = document.getElementById('node-input-mode').value || 'analog';
// Channels JSON (digital). We store the raw string and let the
// server-side nodeClass.js parse it so we can surface parse errors
// at deploy time instead of silently dropping bad config.
node.channels = document.getElementById('node-input-channels').value || '[]';
// Analog smoothing method.
node.smooth_method = document.getElementById('node-input-smooth_method').value || '';
2025-05-14 10:31:50 +02:00
fix(editor): make Input Mode the top-level switch, hide wrong-mode fields
Prior behaviour: the Mode dropdown existed but nothing consumed it in the
editor — analog fields (Scaling, Source Min/Max, Smoothing, …) were
always visible, and the Channels JSON editor was always visible too.
For a legacy node with no saved mode the dropdown defaulted blank so
users reported "I cant even select digital or analog".
Changes:
- Initialize the Mode <select> from node.mode with an 'analog' fallback
for legacy nodes (safe default — matches pre-digital behaviour).
- Wrap analog-only fields and digital-only fields in labelled containers
and toggle their display based on the selected mode. Mode change is
live — no redeploy needed to see the right form.
- Inline hint under the Mode dropdown tells the user what payload shape
is expected for the current mode.
- Channels JSON gets live validation — shows channel count + names on
valid JSON, warns on missing key/type, errors on invalid JSON.
- Label function appends ' [digital]' so the node visibly differs in a
flow from an analog sibling.
- oneditsave is mode-aware: only warns about incomplete scaling ranges
in analog mode; in digital mode warns if the channels array is empty
or unparseable.
Runtime friendliness:
- nodeClass node-status now shows 'digital · N channel(s)' on startup in
digital mode, and 'digital · N/M ch updated' after each incoming msg
so the editor has a live heartbeat even when there is no single scalar.
- When analog mode receives an object payload (or digital receives a
number), the node logs an actionable warn suggesting the mode switch
instead of silently dropping the message.
Explicit, not auto-detected: mode remains a deployment-time choice
because the two modes take different editor config (scaling/smoothing vs
channels map). Auto-detecting at runtime would leave the node
unconfigured in whichever mode the user hadn't anticipated.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:00:34 +02:00
// Save checkbox properties (always safe to read regardless of mode;
// these elements exist in the DOM even when their section is hidden).
2025-06-24 10:48:40 +02:00
["scaling", "simulator"].forEach(
2025-05-14 10:31:50 +02:00
(field) => (node[field] = document.getElementById(`node-input-${field}`).checked)
);
["i_min", "i_max", "i_offset", "o_min", "o_max", "count"].forEach(
(field) => (node[field] = parseFloat(document.getElementById(`node-input-${field}`).value) || 0)
);
fix(editor): make Input Mode the top-level switch, hide wrong-mode fields
Prior behaviour: the Mode dropdown existed but nothing consumed it in the
editor — analog fields (Scaling, Source Min/Max, Smoothing, …) were
always visible, and the Channels JSON editor was always visible too.
For a legacy node with no saved mode the dropdown defaulted blank so
users reported "I cant even select digital or analog".
Changes:
- Initialize the Mode <select> from node.mode with an 'analog' fallback
for legacy nodes (safe default — matches pre-digital behaviour).
- Wrap analog-only fields and digital-only fields in labelled containers
and toggle their display based on the selected mode. Mode change is
live — no redeploy needed to see the right form.
- Inline hint under the Mode dropdown tells the user what payload shape
is expected for the current mode.
- Channels JSON gets live validation — shows channel count + names on
valid JSON, warns on missing key/type, errors on invalid JSON.
- Label function appends ' [digital]' so the node visibly differs in a
flow from an analog sibling.
- oneditsave is mode-aware: only warns about incomplete scaling ranges
in analog mode; in digital mode warns if the channels array is empty
or unparseable.
Runtime friendliness:
- nodeClass node-status now shows 'digital · N channel(s)' on startup in
digital mode, and 'digital · N/M ch updated' after each incoming msg
so the editor has a live heartbeat even when there is no single scalar.
- When analog mode receives an object payload (or digital receives a
number), the node logs an actionable warn suggesting the mode switch
instead of silently dropping the message.
Explicit, not auto-detected: mode remains a deployment-time choice
because the two modes take different editor config (scaling/smoothing vs
channels map). Auto-detecting at runtime would leave the node
unconfigured in whichever mode the user hadn't anticipated.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:00:34 +02:00
// Mode-dependent validation. In digital mode we don't care about
// scaling completeness (the channels have their own per-channel
// scaling); in analog mode we still warn about half-filled ranges.
if (node.mode === 'analog' & & node.scaling & & (isNaN(node.i_min) || isNaN(node.i_max))) {
2025-05-14 10:31:50 +02:00
RED.notify("Scaling enabled, but input range is incomplete!", "error");
}
fix(editor): make Input Mode the top-level switch, hide wrong-mode fields
Prior behaviour: the Mode dropdown existed but nothing consumed it in the
editor — analog fields (Scaling, Source Min/Max, Smoothing, …) were
always visible, and the Channels JSON editor was always visible too.
For a legacy node with no saved mode the dropdown defaulted blank so
users reported "I cant even select digital or analog".
Changes:
- Initialize the Mode <select> from node.mode with an 'analog' fallback
for legacy nodes (safe default — matches pre-digital behaviour).
- Wrap analog-only fields and digital-only fields in labelled containers
and toggle their display based on the selected mode. Mode change is
live — no redeploy needed to see the right form.
- Inline hint under the Mode dropdown tells the user what payload shape
is expected for the current mode.
- Channels JSON gets live validation — shows channel count + names on
valid JSON, warns on missing key/type, errors on invalid JSON.
- Label function appends ' [digital]' so the node visibly differs in a
flow from an analog sibling.
- oneditsave is mode-aware: only warns about incomplete scaling ranges
in analog mode; in digital mode warns if the channels array is empty
or unparseable.
Runtime friendliness:
- nodeClass node-status now shows 'digital · N channel(s)' on startup in
digital mode, and 'digital · N/M ch updated' after each incoming msg
so the editor has a live heartbeat even when there is no single scalar.
- When analog mode receives an object payload (or digital receives a
number), the node logs an actionable warn suggesting the mode switch
instead of silently dropping the message.
Explicit, not auto-detected: mode remains a deployment-time choice
because the two modes take different editor config (scaling/smoothing vs
channels map). Auto-detecting at runtime would leave the node
unconfigured in whichever mode the user hadn't anticipated.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:00:34 +02:00
if (node.mode === 'digital') {
try {
const parsed = JSON.parse(node.channels || '[]');
if (!Array.isArray(parsed) || parsed.length === 0) {
RED.notify("Digital mode: no channels defined. The node will emit nothing.", "warning");
}
} catch (e) {
RED.notify("Digital mode: Channels JSON is invalid (" + e.message + ")", "error");
}
}
2025-05-14 10:31:50 +02:00
},
});
< / script >
<!-- Main UI -->
< script type = "text/html" data-template-name = "measurement" >
2025-06-25 10:43:15 +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
<!-- Input mode -->
< div class = "form-row" >
< label for = "node-input-mode" > < i class = "fa fa-exchange" > < / i > Input Mode< / label >
< select id = "node-input-mode" style = "width:60%;" >
< option value = "analog" > analog — one scalar per msg.payload (classic PLC)< / option >
< option value = "digital" > digital — object payload with many channel keys (MQTT/IoT)< / option >
< / select >
< / div >
fix(editor): make Input Mode the top-level switch, hide wrong-mode fields
Prior behaviour: the Mode dropdown existed but nothing consumed it in the
editor — analog fields (Scaling, Source Min/Max, Smoothing, …) were
always visible, and the Channels JSON editor was always visible too.
For a legacy node with no saved mode the dropdown defaulted blank so
users reported "I cant even select digital or analog".
Changes:
- Initialize the Mode <select> from node.mode with an 'analog' fallback
for legacy nodes (safe default — matches pre-digital behaviour).
- Wrap analog-only fields and digital-only fields in labelled containers
and toggle their display based on the selected mode. Mode change is
live — no redeploy needed to see the right form.
- Inline hint under the Mode dropdown tells the user what payload shape
is expected for the current mode.
- Channels JSON gets live validation — shows channel count + names on
valid JSON, warns on missing key/type, errors on invalid JSON.
- Label function appends ' [digital]' so the node visibly differs in a
flow from an analog sibling.
- oneditsave is mode-aware: only warns about incomplete scaling ranges
in analog mode; in digital mode warns if the channels array is empty
or unparseable.
Runtime friendliness:
- nodeClass node-status now shows 'digital · N channel(s)' on startup in
digital mode, and 'digital · N/M ch updated' after each incoming msg
so the editor has a live heartbeat even when there is no single scalar.
- When analog mode receives an object payload (or digital receives a
number), the node logs an actionable warn suggesting the mode switch
instead of silently dropping the message.
Explicit, not auto-detected: mode remains a deployment-time choice
because the two modes take different editor config (scaling/smoothing vs
channels map). Auto-detecting at runtime would leave the node
unconfigured in whichever mode the user hadn't anticipated.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:00:34 +02:00
< div class = "form-row" id = "mode-hint" style = "margin-left:105px; font-size:12px; color:#666;" > < / div >
<!-- ===================== DIGITAL MODE FIELDS ===================== -->
< div id = "digital-only-fields" >
< div class = "form-row" id = "row-input-channels" >
< label for = "node-input-channels" > < i class = "fa fa-list" > < / i > Channels (JSON)< / label >
< textarea id = "node-input-channels" rows = "6" style = "width:60%; font-family:monospace;" placeholder = '[{"key":"temperature","type":"temperature","position":"atEquipment","unit":"C","scaling":{"enabled":false,"inputMin":0,"inputMax":1,"absMin":-50,"absMax":150,"offset":0},"smoothing":{"smoothWindow":5,"smoothMethod":"mean"}}]' > < / textarea >
< div class = "form-tips" > One entry per payload key. Each channel has its own type / position / unit / scaling / smoothing / outlier detection. See README for the full schema.< / div >
< / div >
< div class = "form-row" id = "channels-validation" style = "margin-left:105px; font-size:12px;" > < / div >
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
< / div >
fix(editor): make Input Mode the top-level switch, hide wrong-mode fields
Prior behaviour: the Mode dropdown existed but nothing consumed it in the
editor — analog fields (Scaling, Source Min/Max, Smoothing, …) were
always visible, and the Channels JSON editor was always visible too.
For a legacy node with no saved mode the dropdown defaulted blank so
users reported "I cant even select digital or analog".
Changes:
- Initialize the Mode <select> from node.mode with an 'analog' fallback
for legacy nodes (safe default — matches pre-digital behaviour).
- Wrap analog-only fields and digital-only fields in labelled containers
and toggle their display based on the selected mode. Mode change is
live — no redeploy needed to see the right form.
- Inline hint under the Mode dropdown tells the user what payload shape
is expected for the current mode.
- Channels JSON gets live validation — shows channel count + names on
valid JSON, warns on missing key/type, errors on invalid JSON.
- Label function appends ' [digital]' so the node visibly differs in a
flow from an analog sibling.
- oneditsave is mode-aware: only warns about incomplete scaling ranges
in analog mode; in digital mode warns if the channels array is empty
or unparseable.
Runtime friendliness:
- nodeClass node-status now shows 'digital · N channel(s)' on startup in
digital mode, and 'digital · N/M ch updated' after each incoming msg
so the editor has a live heartbeat even when there is no single scalar.
- When analog mode receives an object payload (or digital receives a
number), the node logs an actionable warn suggesting the mode switch
instead of silently dropping the message.
Explicit, not auto-detected: mode remains a deployment-time choice
because the two modes take different editor config (scaling/smoothing vs
channels map). Auto-detecting at runtime would leave the node
unconfigured in whichever mode the user hadn't anticipated.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:00:34 +02:00
<!-- ===================== ANALOG MODE FIELDS ===================== -->
< div id = "analog-only-fields" >
< hr >
<!-- Scaling Checkbox -->
< div class = "form-row" >
< label for = "node-input-scaling"
>< i class = "fa fa-compress" > < / i > Scaling< / label >
< input type = "checkbox" id = "node-input-scaling" style = "width:20px; vertical-align:baseline;" / >
< span > Enable input scaling?< / span >
< / div >
<!-- Source Min/Max (only if scaling is true) -->
< div class = "form-row" id = "row-input-i_min" >
< label for = "node-input-i_min" > < i class = "fa fa-arrow-down" > < / i > Source Min< / label >
< input type = "number" id = "node-input-i_min" placeholder = "0" / >
< / div >
< div class = "form-row" id = "row-input-i_max" >
< label for = "node-input-i_max" > < i class = "fa fa-arrow-up" > < / i > Source Max< / label >
< input type = "number" id = "node-input-i_max" placeholder = "3000" / >
< / div >
<!-- Offset -->
< div class = "form-row" >
< label for = "node-input-i_offset" > < i class = "fa fa-adjust" > < / i > Input Offset< / label >
< input type = "number" id = "node-input-i_offset" placeholder = "0" / >
< / div >
<!-- Output / Process Min/Max -->
< div class = "form-row" >
< label for = "node-input-o_min" > < i class = "fa fa-tag" > < / i > Process Min< / label >
< input type = "number" id = "node-input-o_min" placeholder = "0" / >
< / div >
< div class = "form-row" >
< label for = "node-input-o_max" > < i class = "fa fa-tag" > < / i > Process Max< / label >
< input type = "number" id = "node-input-o_max" placeholder = "1" / >
< / div >
<!-- Simulator Checkbox -->
< div class = "form-row" >
< label for = "node-input-simulator" > < i class = "fa fa-cog" > < / i > Simulator< / label >
< input type = "checkbox" id = "node-input-simulator" style = "width:20px; vertical-align:baseline;" / >
< span > Activate internal simulation?< / span >
< / div >
<!-- Smoothing Method -->
< div class = "form-row" >
< label for = "node-input-smooth_method" > < i class = "fa fa-line-chart" > < / i > Smoothing< / label >
< select id = "node-input-smooth_method" style = "width:60%;" >
< / select >
< / div >
<!-- Smoothing Window -->
< div class = "form-row" >
< label for = "node-input-count" > Window< / label >
< input type = "number" id = "node-input-count" placeholder = "10" style = "width:60px;" / >
< div class = "form-tips" > Number of samples for smoothing< / div >
< / div >
2025-05-14 10:31:50 +02:00
< / div >
2026-03-12 16:39:25 +01:00
< hr >
< h3 > Output Formats< / h3 >
< div class = "form-row" >
< label for = "node-input-processOutputFormat" > < i class = "fa fa-random" > < / i > Process Output< / label >
< select id = "node-input-processOutputFormat" style = "width:60%;" >
< option value = "process" > process< / option >
< option value = "json" > json< / option >
< option value = "csv" > csv< / option >
< / select >
< / div >
< div class = "form-row" >
< label for = "node-input-dbaseOutputFormat" > < i class = "fa fa-database" > < / i > Database Output< / label >
< select id = "node-input-dbaseOutputFormat" style = "width:60%;" >
< option value = "influxdb" > influxdb< / option >
< option value = "json" > json< / option >
< option value = "csv" > csv< / option >
< / select >
< / div >
2025-06-20 17:14:22 +02:00
<!-- Optional Extended Fields: supplier, cat, type, model, unit -->
2025-06-25 10:43:15 +02:00
<!-- Asset fields will be injected here -->
< div id = "asset-fields-placeholder" > < / div >
2025-05-14 10:31:50 +02:00
<!-- loglevel checkbox -->
2025-06-25 10:43:15 +02:00
< div id = "logger-fields-placeholder" > < / div >
2025-05-14 10:31:50 +02:00
2025-06-25 10:43:15 +02:00
<!-- Position fields will be injected here -->
< div id = "position-fields-placeholder" > < / div >
2025-05-14 10:31:50 +02:00
< / script >
< script type = "text/html" data-help-name = "measurement" >
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
< p > < b > Measurement< / b > : signal conditioning for a sensor or a bundle of sensors. Runs offset → scaling → smoothing → outlier filtering on each incoming value and publishes into the shared < code > MeasurementContainer< / code > .< / p >
< h3 > Input modes< / h3 >
< ul >
< li > < b > analog< / b > (default) — < code > msg.payload< / code > is a single number (PLC / 4-20 mA style). One pipeline, one output measurement.< / li >
< li > < b > digital< / b > — < code > msg.payload< / code > is an object with many keys (MQTT / JSON IoT). Each key maps to its own < i > channel< / i > with independent scaling, smoothing, outlier detection, type, position, unit. One message → N measurements.< / li >
< / ul >
< h3 > Topics (< code > msg.topic< / code > )< / h3 >
< ul >
< li > < code > measurement< / code > — main input. analog: number; digital: object keyed by channel names.< / li >
< li > < code > simulator< / code > — toggle the internal random-walk source.< / li >
< li > < code > outlierDetection< / code > — toggle the outlier filter.< / li >
< li > < code > calibrate< / code > — set offset so current output matches < code > Source Min< / code > (scaling on) / < code > Process Min< / code > (scaling off). Requires a stable window.< / li >
< / ul >
< h3 > Output ports< / h3 >
< ol >
< li > < b > process< / b > — delta-compressed payload. analog: < code > {mAbs, mPercent, totalMinValue, totalMaxValue, totalMinSmooth, totalMaxSmooth}< / code > . digital: < code > {channels: { key: {...} }}< / code > .< / li >
< li > < b > dbase< / b > — InfluxDB line-protocol telemetry.< / li >
< li > < b > parent< / b > — < code > registerChild< / code > handshake for the parent equipment node.< / li >
< / ol >
< h3 > Analog configuration< / h3 >
< ul >
< li > < b > Scaling< / b > : enables linear interpolation from < code > [Source Min, Source Max]< / code > to < code > [Process Min, Process Max]< / code > .< / li >
< li > < b > Input Offset< / b > : additive bias applied before scaling.< / li >
< li > < b > Smoothing< / b > : < code > none< / code > | < code > mean< / code > | < code > min< / code > | < code > max< / code > | < code > sd< / code > | < code > lowPass< / code > | < code > highPass< / code > | < code > weightedMovingAverage< / code > | < code > bandPass< / code > | < code > median< / code > | < code > kalman< / code > | < code > savitzkyGolay< / code > .< / li >
< li > < b > Window< / b > : sample count for the smoothing window.< / li >
< li > < b > Outlier detection< / b > (via < code > outlierDetection< / code > topic toggle): < code > zScore< / code > , < code > iqr< / code > , < code > modifiedZScore< / code > .< / li >
< / ul >
< h3 > Digital configuration< / h3 >
< p > Populate the < b > Channels (JSON)< / b > field with an array. Each entry:< / p >
< pre > {
"key": "temperature",
"type": "temperature",
"position": "atEquipment",
"unit": "C",
"scaling": { "enabled": false, "inputMin": 0, "inputMax": 1, "absMin": -50, "absMax": 150, "offset": 0 },
"smoothing": { "smoothWindow": 5, "smoothMethod": "mean" },
"outlierDetection": { "enabled": true, "method": "zScore", "threshold": 3 }
}< / pre >
< p > < code > scaling< / code > , < code > smoothing< / code > , < code > outlierDetection< / code > are optional — missing sections fall back to the analog-mode fields above.< / p >
< p > Unknown < code > type< / code > values (anything not in < code > pressure/flow/power/temperature/volume/length/mass/energy< / code > ) are accepted without unit compatibility checks, so user-defined channels like < code > humidity< / code > , < code > co2< / code > , < code > voc< / code > work out of the box.< / p >
2025-05-14 10:31:50 +02:00
< / script >