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
-->
2026-05-28 08:59:28 +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 -->
<!-- Editor JS modules — see nodes/measurement/src/editor/. Loaded in
dependency order: index.js (namespace + helpers) → visuals → handlers. -->
< script src = "/measurement/editor/index.js" > < / script >
< script src = "/measurement/editor/hover-couple.js" > < / script >
< script src = "/measurement/editor/pipeline-diagram.js" > < / script >
< script src = "/measurement/editor/scaling-chart.js" > < / script >
< script src = "/measurement/editor/smoothing-sparkline.js" > < / script >
< script src = "/measurement/editor/digital-channels.js" > < / script >
< script src = "/measurement/editor/oneditprepare.js" > < / script >
< script src = "/measurement/editor/oneditsave.js" > < / script >
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",
2026-05-21 15:06:37 +02:00
color: "#D4A02E",
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-05-11 17:29:15 +02:00
stabilityThreshold: { value: 0.01 },
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
},
2026-05-28 08:59:28 +02:00
oneditprepare: function () { window.MeasEditor.oneditprepare.call(this); },
oneditsave: function () { window.MeasEditor.oneditsave.call(this); },
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
2026-05-28 08:59:28 +02:00
< style >
/* === Section headers ============================================== */
.meas-section { margin-top: 8px; }
.meas-section h4 { margin: 14px 0 6px 0; }
.meas-help {
font-size: 12px; color: #777; margin: 0 0 8px 0;
}
/* === Mode cards =================================================== */
.meas-mode-cards { display: flex; gap: 10px; margin: 6px 0 8px 0; }
.meas-mode-card {
flex: 1; cursor: pointer;
border: 2px solid #ccc; border-radius: 6px;
padding: 10px 12px; background: #fff;
transition: border-color 80ms, background 80ms;
}
.meas-mode-card:hover { border-color: #888; background: #fafafa; }
.meas-mode-card.meas-mode-active {
border-color: #0c99d9; background: #f0f8ff;
}
.meas-mode-card .meas-mode-title {
font-weight: 600; font-size: 13px; color: #222;
}
.meas-mode-card .meas-mode-sub {
font-size: 11px; color: #666; margin-top: 4px;
}
.meas-mode-card .meas-mode-payload {
font-family: monospace; font-size: 11px; color: #1F4E79;
margin-top: 4px; background: #f4f8fc; padding: 2px 6px;
border-radius: 3px; display: inline-block;
}
/* === Pipeline diagram ============================================= */
.meas-pipeline-svg {
display: block; width: 100%; max-width: 720px;
background: #fff; border: 1px solid #e5e5e5; border-radius: 4px;
}
.meas-stage rect {
transition: opacity 80ms, stroke-width 80ms;
}
.meas-stage-disabled rect { opacity: 0.35; }
.meas-stage-disabled text { opacity: 0.5; }
.meas-stage-highlight rect {
stroke-width: 3 !important; stroke: #0c99d9 !important;
}
/* === Two-column diag layout (used by scaling chart) =============== */
.meas-diag { display: flex; gap: 24px; align-items: flex-start; margin: 0 0 10px 0; flex-wrap: wrap; }
.meas-diag-side { width: 250px; flex: 0 0 250px; display: flex; flex-direction: column; gap: 5px; }
.meas-diag-side .meas-row {
display: grid; grid-template-columns: minmax(0, 1fr) 80px 16px; align-items: center;
gap: 6px; padding: 4px 6px 4px 10px; border-left: 4px solid #ccc;
background: #fafafa; border-radius: 3px; font-size: 11px;
min-width: 0;
}
.meas-diag-side .meas-row:hover { background: #f0f0f0; }
.meas-diag-side .meas-row label { font-weight: 600; margin: 0; line-height: 1.2; }
.meas-diag-side .meas-row .meas-sub {
grid-column: 1; font-size: 10px; color: #888; font-weight: 400;
}
.meas-diag-side .meas-row input[type=number] {
width: 100%; height: 22px; box-sizing: border-box; font-size: 11px;
padding: 1px 4px; margin: 0; border: 1px solid #ccc; border-radius: 3px;
background: #fff;
}
.meas-diag-side .meas-row input[type=number]:focus {
outline: 1px solid #0c99d9; border-color: #0c99d9;
}
.meas-diag-side .meas-row .meas-unit { color: #888; font-size: 10px; }
.meas-diag-svg-wrap { flex: 1; min-width: 240px; }
/* Border colour per stage so the side-row matches its SVG stage. */
.meas-row[data-stroke="#1F4E79"] { border-left-color: #1F4E79; }
.meas-row[data-stroke="#1E8449"] { border-left-color: #1E8449; }
.meas-row[data-stroke="#D68910"] { border-left-color: #D68910; }
.meas-row[data-stroke="#7D3C98"] { border-left-color: #7D3C98; }
.meas-row[data-stroke="#C0392B"] { border-left-color: #C0392B; }
/* === Digital channel cards ======================================= */
.meas-ch-empty {
font-size: 12px; color: #888; font-style: italic;
padding: 10px 12px; background: #fafafa; border: 1px dashed #ddd;
border-radius: 4px;
}
.meas-ch-card {
border: 1px solid #ddd; border-radius: 4px;
background: #fff; margin-bottom: 6px;
}
.meas-ch-head {
display: grid;
grid-template-columns: 36px minmax(0, 1fr) 110px 110px minmax(0, 1fr) 70px 28px;
gap: 6px; align-items: center;
padding: 6px 8px;
}
.meas-ch-num-badge {
font-size: 10px; color: #888; font-family: monospace;
text-align: center;
}
.meas-ch-input {
height: 24px; box-sizing: border-box; font-size: 12px;
padding: 2px 5px; margin: 0; border: 1px solid #ccc; border-radius: 3px;
background: #fff; min-width: 0;
}
.meas-ch-input:focus { outline: 1px solid #0c99d9; border-color: #0c99d9; }
.meas-ch-input.meas-ch-err { border-color: #C0392B; background: #fdecea; }
.meas-ch-num { width: 100%; }
/* Unit cell wraps either a < select > (canonical type) or a free-text
< input > (custom type). Type-change swaps the wrapper's contents
without rerendering the rest of the card. Make the inner element
fill the grid cell. */
.meas-ch-unit-cell { min-width: 0; }
.meas-ch-unit-cell > * { width: 100%; }
.meas-ch-btn {
height: 24px; box-sizing: border-box;
padding: 0 8px; border: 1px solid #ccc; border-radius: 3px;
background: #f5f5f5; cursor: pointer; font-size: 11px;
}
.meas-ch-btn:hover { background: #ececec; }
.meas-ch-btn-del {
width: 28px; padding: 0; color: #C0392B; font-weight: bold;
}
.meas-ch-btn-del:hover { background: #fdecea; }
.meas-ch-adv {
display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px;
padding: 8px 10px 10px 44px; border-top: 1px solid #eee;
background: #fafbfd;
}
.meas-ch-sub {
background: #fff; border: 1px solid #eee; border-radius: 3px;
padding: 6px 8px;
}
.meas-ch-sub-title {
font-size: 11px; font-weight: 600; color: #444; margin-bottom: 4px;
}
.meas-ch-sub-grid {
display: grid; grid-template-columns: auto 1fr; gap: 4px 6px;
align-items: center;
}
.meas-ch-sub-grid label { font-size: 10px; color: #666; margin: 0; }
.meas-ch-sub-grid.meas-ch-dim { opacity: 0.4; pointer-events: none; }
.meas-ch-cb {
font-size: 11px; font-weight: 600; color: #444;
display: inline-flex; align-items: center; gap: 4px; margin: 0;
}
.meas-ch-actions {
display: flex; gap: 8px; align-items: center; margin: 8px 0;
}
.meas-ch-actions .meas-ch-btn-add {
background: #1E8449; color: #fff; border-color: #186b3a;
}
.meas-ch-actions .meas-ch-btn-add:hover { background: #186b3a; }
< / style >
<!-- ================================================================ -->
<!-- INPUT MODE -->
<!-- ================================================================ -->
< div class = "meas-section" >
< h4 > Input mode< / h4 >
< p class = "meas-help" > Pick how this node should interpret < code > msg.payload< / code > . Click a card to switch — the dropdown stays in sync.< / p >
< div class = "meas-mode-cards" >
< div class = "meas-mode-card" data-mode = "analog" >
< div class = "meas-mode-title" > < i class = "fa fa-tachometer" > < / i > Analog< / div >
< div class = "meas-mode-sub" > One scalar per message (classic PLC / 4– 20 mA).< / div >
< div class = "meas-mode-payload" > msg.payload = 22.5< / div >
< / div >
< div class = "meas-mode-card" data-mode = "digital" >
< div class = "meas-mode-title" > < i class = "fa fa-sitemap" > < / i > Digital< / div >
< div class = "meas-mode-sub" > Object payload, many channels per message (MQTT / IoT).< / div >
< div class = "meas-mode-payload" > msg.payload = {"temperature": 22.5, "humidity": 45}< / div >
< / div >
< / div >
< div class = "form-row" >
< label for = "node-input-mode" > < i class = "fa fa-exchange" > < / i > 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 >
< div class = "form-row" id = "mode-hint" style = "margin-left:105px; font-size:12px; color:#666;" > < / div >
< / div >
<!-- ================================================================ -->
<!-- ANALOG PIPELINE DIAGRAM (top of the analog block) -->
<!-- ================================================================ -->
< div id = "meas-pipeline-wrap" class = "meas-section" >
< h4 > Signal pipeline< / h4 >
< p class = "meas-help" >
Each incoming value flows through these stages. Stages dim when they're
switched off below. Hover an input row (offset / scale / smoothing) to
highlight the matching stage.
< / p >
<!--
============================================================
PIPELINE FLOW SVG
============================================================
viewBox 720 x 140. Six stages, equal width, horizontal arrows.
Stage stroke + sub-label are updated by pipelineDiagram.redraw().
Hover-couple targets the < g class = "meas-stage" id = "meas-stage-*" > group.
============================================================
-->
< svg class = "meas-pipeline-svg" xmlns = "http://www.w3.org/2000/svg" viewBox = "0 0 720 140"
font-family="Arial,sans-serif" font-size="11">
< defs >
< marker id = "meas-arrow" viewBox = "0 0 10 10" refX = "9" refY = "5"
markerWidth="7" markerHeight="7" orient="auto-start-reverse">
< path d = "M 0 0 L 10 5 L 0 10 z" fill = "#555" / >
< / marker >
< / defs >
<!-- Six stage boxes at x = 8, 128, 248, 368, 488, 608 (width=104, gap=16) -->
< g class = "meas-stage" id = "meas-stage-input" >
< rect x = "8" y = "35" width = "104" height = "70" rx = "6" fill = "#eef6fb" stroke = "#1F4E79" stroke-width = "1.5" / >
< text x = "60" y = "60" text-anchor = "middle" fill = "#1F4E79" font-weight = "bold" > msg.payload< / text >
< text x = "60" y = "78" text-anchor = "middle" fill = "#555" font-size = "10" > number< / text >
< text x = "60" y = "94" text-anchor = "middle" fill = "#888" font-size = "9" > topic: measurement< / text >
< / g >
< g class = "meas-stage" id = "meas-stage-offset" >
< rect x = "128" y = "35" width = "104" height = "70" rx = "6" fill = "#fdf4e7" stroke = "#D68910" stroke-width = "1.5" / >
< text x = "180" y = "60" text-anchor = "middle" fill = "#D68910" font-weight = "bold" > + offset< / text >
< text id = "meas-stage-offset-sub" x = "180" y = "78" text-anchor = "middle" fill = "#555" font-size = "10" > off< / text >
< text x = "180" y = "94" text-anchor = "middle" fill = "#888" font-size = "9" > additive bias< / text >
< / g >
< g class = "meas-stage" id = "meas-stage-scale" >
< rect x = "248" y = "35" width = "104" height = "70" rx = "6" fill = "#eafaf1" stroke = "#1E8449" stroke-width = "1.5" / >
< text x = "300" y = "60" text-anchor = "middle" fill = "#1E8449" font-weight = "bold" > scale< / text >
< text id = "meas-stage-scale-sub" x = "300" y = "78" text-anchor = "middle" fill = "#555" font-size = "10" > off< / text >
< text x = "300" y = "94" text-anchor = "middle" fill = "#888" font-size = "9" > [in]→[out]< / text >
< / g >
< g class = "meas-stage" id = "meas-stage-smooth" >
< rect x = "368" y = "35" width = "104" height = "70" rx = "6" fill = "#eef2fb" stroke = "#1F4E79" stroke-width = "1.5" / >
< text x = "420" y = "60" text-anchor = "middle" fill = "#1F4E79" font-weight = "bold" > smooth< / text >
< text id = "meas-stage-smooth-sub" x = "420" y = "78" text-anchor = "middle" fill = "#555" font-size = "10" > off< / text >
< text x = "420" y = "94" text-anchor = "middle" fill = "#888" font-size = "9" > rolling window< / text >
< / g >
< g class = "meas-stage" id = "meas-stage-outlier" >
< rect x = "488" y = "35" width = "104" height = "70" rx = "6" fill = "#fdecea" stroke = "#C0392B" stroke-width = "1.5" / >
< text x = "540" y = "60" text-anchor = "middle" fill = "#C0392B" font-weight = "bold" > outlier< / text >
< text x = "540" y = "78" text-anchor = "middle" fill = "#555" font-size = "10" > runtime toggle< / text >
< text x = "540" y = "94" text-anchor = "middle" fill = "#888" font-size = "9" > topic: outlierDetection< / text >
< / g >
< g class = "meas-stage" id = "meas-stage-output" >
< rect x = "608" y = "35" width = "104" height = "70" rx = "6" fill = "#f4f4f4" stroke = "#333" stroke-width = "1.5" / >
< text x = "660" y = "60" text-anchor = "middle" fill = "#333" font-weight = "bold" > output< / text >
< text id = "meas-stage-output-sub" x = "660" y = "78" text-anchor = "middle" fill = "#555" font-size = "10" > process / influxdb< / text >
< text x = "660" y = "94" text-anchor = "middle" fill = "#888" font-size = "9" > port 0 / port 1< / text >
< / g >
<!-- Arrows between stages -->
< line x1 = "112" y1 = "70" x2 = "128" y2 = "70" stroke = "#555" marker-end = "url(#meas-arrow)" / >
< line x1 = "232" y1 = "70" x2 = "248" y2 = "70" stroke = "#555" marker-end = "url(#meas-arrow)" / >
< line x1 = "352" y1 = "70" x2 = "368" y2 = "70" stroke = "#555" marker-end = "url(#meas-arrow)" / >
< line x1 = "472" y1 = "70" x2 = "488" y2 = "70" stroke = "#555" marker-end = "url(#meas-arrow)" / >
< line x1 = "592" y1 = "70" x2 = "608" y2 = "70" stroke = "#555" marker-end = "url(#meas-arrow)" / >
<!-- Top caption -->
< text x = "360" y = "20" text-anchor = "middle" fill = "#444" font-size = "11" font-style = "italic" >
analog signal pipeline (digital mode runs one pipeline per channel)
< / text >
<!-- Hover hint -->
< text x = "360" y = "128" text-anchor = "middle" fill = "#888" font-size = "10" >
hover an input row below → its stage highlights
< / text >
< / svg >
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
<!-- ===================== DIGITAL MODE FIELDS ===================== -->
2026-05-28 08:59:28 +02:00
< div id = "digital-only-fields" class = "meas-section" >
< h4 > Digital channels< / h4 >
< p class = "meas-help" >
Define one entry per key in < code > msg.payload< / code > . Each channel has its
own type, position, unit, and optional scaling / smoothing / outlier
detection (click < b > ▾ more< / b > to reveal). The analog settings further
down are ignored in digital mode.
< / p >
<!-- Row editor — rendered by src/editor/digital - channels.js. The raw
textarea below is kept in sync on every edit (it remains the source
of truth on the node). -->
< div id = "meas-channels-rows" > < / div >
< div class = "meas-ch-actions" >
< button type = "button" id = "meas-channels-add" class = "meas-ch-btn meas-ch-btn-add" >
+ Add channel
< / button >
< button type = "button" id = "meas-channels-raw-toggle" class = "meas-ch-btn" >
▾ Show raw JSON
< / button >
< / div >
<!-- Raw JSON escape - hatch. Hidden by default; toggle button reveals it
for power-users that want to paste / bulk-edit. Validation below
(channels-validation) fires on every textarea input event. -->
< div id = "meas-channels-raw" style = "display:none;" >
< div class = "form-row" id = "row-input-channels" >
< label for = "node-input-channels" > < i class = "fa fa-code" > < / i > Channels (JSON)< / label >
< textarea id = "node-input-channels" rows = "8" 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" > The row editor above mirrors edits into this field — usually you won't need to touch it directly.< / div >
< / 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 >
< 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" >
2026-05-28 08:59:28 +02:00
<!-- ============================================================ -->
<!-- SCALING -->
<!-- ============================================================ -->
< div class = "meas-section" >
< h4 > Scaling< / h4 >
< p class = "meas-help" >
Map the raw input range (e.g. 4– 20 mA, 0– 3000 counts) to a physical
process range (e.g. 0– 10 bar). Apply an offset first to zero-correct
the sensor.
< / p >
< 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 linear input scaling< / span >
< / div >
< 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 class = "form-tips" > Applied before scaling (additive bias).< / div >
< / div >
< div class = "meas-diag" id = "meas-scaling-wrap" >
< div class = "meas-diag-side" id = "meas-scaling-inputs" >
< div class = "meas-row" data-stroke = "#1F4E79" data-couples-line = "meas-scale-input-axis" >
< div > < label > Source Min< / label > < div class = "meas-sub" > raw input low< / div > < / div >
< input type = "number" id = "node-input-i_min" placeholder = "0" / >
< span class = "meas-unit" > raw< / span >
< / div >
< div class = "meas-row" data-stroke = "#1F4E79" data-couples-line = "meas-scale-input-axis" >
< div > < label > Source Max< / label > < div class = "meas-sub" > raw input high< / div > < / div >
< input type = "number" id = "node-input-i_max" placeholder = "3000" / >
< span class = "meas-unit" > raw< / span >
< / div >
< div class = "meas-row" data-stroke = "#1E8449" data-couples-line = "meas-scale-output-axis" >
< div > < label > Process Min< / label > < div class = "meas-sub" > scaled output low< / div > < / div >
< input type = "number" id = "node-input-o_min" placeholder = "0" / >
< span class = "meas-unit" > eng< / span >
< / div >
< div class = "meas-row" data-stroke = "#1E8449" data-couples-line = "meas-scale-output-axis" >
< div > < label > Process Max< / label > < div class = "meas-sub" > scaled output high< / div > < / div >
< input type = "number" id = "node-input-o_max" placeholder = "1" / >
< span class = "meas-unit" > eng< / span >
< / div >
< / div >
<!--
============================================================
SCALING LINEAR-TRANSFORM CHART
============================================================
viewBox 300 x 180. Axes at left=44, right=286, top=14, bot=156.
Line endpoints are placed by scalingChart.redraw().
============================================================
-->
< div class = "meas-diag-svg-wrap" >
< svg xmlns = "http://www.w3.org/2000/svg" viewBox = "0 0 300 180"
style="display:block;width:100%;max-width:320px;background:#fff;border:1px solid #e5e5e5;border-radius:4px;"
font-family="Arial,sans-serif" font-size="10">
<!-- Plot frame -->
< rect x = "44" y = "14" width = "242" height = "142" fill = "#fafcff" stroke = "#e5e5e5" / >
<!-- Axes -->
< line id = "meas-scale-input-axis" x1 = "44" y1 = "156" x2 = "286" y2 = "156" stroke = "#1F4E79" stroke-width = "1.5" / >
< line id = "meas-scale-output-axis" x1 = "44" y1 = "156" x2 = "44" y2 = "14" stroke = "#1E8449" stroke-width = "1.5" / >
<!-- Tick labels -->
< text id = "meas-scale-x-min" x = "44" y = "170" text-anchor = "middle" fill = "#1F4E79" > 0< / text >
< text id = "meas-scale-x-max" x = "286" y = "170" text-anchor = "middle" fill = "#1F4E79" > 1< / text >
< text id = "meas-scale-y-min" x = "40" y = "159" text-anchor = "end" fill = "#1E8449" > 0< / text >
< text id = "meas-scale-y-max" x = "40" y = "17" text-anchor = "end" fill = "#1E8449" > 1< / text >
<!-- Axis titles -->
< text x = "165" y = "178" text-anchor = "middle" fill = "#1F4E79" font-style = "italic" > raw input (Source Min → Source Max)< / text >
< text x = "14" y = "85" text-anchor = "middle" fill = "#1E8449" font-style = "italic" transform = "rotate(-90 14 85)" > process value (Process Min → Process Max)< / text >
<!-- The transform line -->
< polyline id = "meas-scale-line" fill = "none" stroke = "#0c99d9" stroke-width = "2.5" points = "44,156 286,14" / >
<!-- Offset readout -->
< text id = "meas-scale-offset-label" x = "165" y = "10" text-anchor = "middle" fill = "#D68910" font-size = "10" > offset: 0 (no shift)< / text >
< / svg >
< / div >
< / 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 >
2026-05-28 08:59:28 +02:00
<!-- ============================================================ -->
<!-- SMOOTHING -->
<!-- ============================================================ -->
< div class = "meas-section" >
< h4 > Smoothing< / h4 >
< p class = "meas-help" >
Reduce noise on the scaled signal. Each method behaves differently
— the preview below shows the result on a fixed noisy test signal.
< / p >
< div class = "meas-diag" >
< div class = "meas-diag-side" >
< div class = "meas-row" data-stroke = "#1F4E79" data-couples-line = "meas-stage-smooth" >
< div > < label > Method< / label > < div class = "meas-sub" > none / mean / median / kalman / …< / div > < / div >
< select id = "node-input-smooth_method" style = "grid-column: 2 / span 2; width: 100%;" > < / select >
< / div >
< div class = "meas-row" data-stroke = "#1F4E79" data-couples-line = "meas-stage-smooth" >
< div > < label > Window< / label > < div class = "meas-sub" > sample count< / div > < / div >
< input type = "number" id = "node-input-count" placeholder = "10" / >
< span class = "meas-unit" > n< / span >
< / div >
< / div >
<!--
============================================================
SMOOTHING SPARKLINE
============================================================
viewBox 390 x 100. Plot range left=10, right=380, top=8, bot=92.
============================================================
-->
< div class = "meas-diag-svg-wrap" id = "meas-smooth-wrap" >
< svg xmlns = "http://www.w3.org/2000/svg" viewBox = "0 0 390 110"
style="display:block;width:100%;max-width:420px;background:#fff;border:1px solid #e5e5e5;border-radius:4px;"
font-family="Arial,sans-serif" font-size="10">
<!-- Plot frame -->
< rect x = "10" y = "8" width = "370" height = "84" fill = "#fafcff" stroke = "#e5e5e5" / >
<!-- Series -->
< polyline id = "meas-smooth-raw" fill = "none" stroke = "#aaa" stroke-width = "1" points = "" / >
< polyline id = "meas-smooth-smoothed" fill = "none" stroke = "#1E8449" stroke-width = "1.8" points = "" / >
<!-- Legend -->
< line x1 = "18" y1 = "103" x2 = "36" y2 = "103" stroke = "#aaa" / >
< text x = "40" y = "106" fill = "#888" > raw (noisy)< / text >
< line x1 = "120" y1 = "103" x2 = "138" y2 = "103" stroke = "#1E8449" stroke-width = "1.8" / >
< text x = "142" y = "106" fill = "#1E8449" > smoothed< / text >
<!-- Method/window readout -->
< text id = "meas-smooth-label" x = "375" y = "106" text-anchor = "end" fill = "#555" font-style = "italic" > no smoothing — raw value passed through< / text >
< / svg >
< / div >
< / 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 >
2026-05-28 08:59:28 +02:00
<!-- ============================================================ -->
<!-- SIMULATION -->
<!-- ============================================================ -->
< div class = "meas-section" >
< h4 > Simulation< / h4 >
< 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 > Replace the real input with an internal random-walk source (toggle at runtime via topic < code > simulator< / code > ).< / span >
< / 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 >
2026-05-28 08:59:28 +02:00
<!-- ============================================================ -->
<!-- CALIBRATION -->
<!-- ============================================================ -->
< div class = "meas-section" >
< h4 > Calibration< / h4 >
< p class = "meas-help" >
The < code > calibrate< / code > topic shifts the offset so the current
output matches the configured low end. It only fires when the rolling
window is "stable enough" — define what that means here.
< / p >
< div class = "form-row" >
< label for = "node-input-stabilityThreshold" > < i class = "fa fa-balance-scale" > < / i > Stability< / label >
< input type = "number" id = "node-input-stabilityThreshold" placeholder = "0.01" step = "any" style = "width:100px;" / >
< span style = "margin-left:6px; color:#666;" > scaling-units< / span >
< div class = "form-tips" > Maximum rolling-window standard deviation that still counts as stable. Default 0.01.< / div >
< / 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 >
2026-05-28 08:59:28 +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
2026-05-28 08:59:28 +02:00
<!-- ================================================================ -->
<!-- OUTPUT FORMATS -->
<!-- ================================================================ -->
< div class = "meas-section" >
< h4 > Output formats< / h4 >
< p class = "meas-help" >
Process port (0) drives downstream control nodes; database port (1)
feeds telemetry/historian. Pick the encoding each consumer expects.
< / p >
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" >
2026-05-28 08:59:28 +02:00
< label for = "node-input-processOutputFormat" > < i class = "fa fa-random" > < / i > Process port< / label >
< select id = "node-input-processOutputFormat" style = "width:60%;" >
< option value = "process" > process< / option >
< option value = "json" > json< / option >
< option value = "csv" > csv< / 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
< / select >
< / div >
< div class = "form-row" >
2026-05-28 08:59:28 +02:00
< label for = "node-input-dbaseOutputFormat" > < i class = "fa fa-database" > < / i > Database port< / label >
< select id = "node-input-dbaseOutputFormat" style = "width:60%;" >
< option value = "influxdb" > influxdb< / option >
< option value = "frost" > frost< / option >
< option value = "json" > json< / option >
< option value = "csv" > csv< / option >
< / select >
2026-05-11 17:29:15 +02:00
< / div >
2025-05-14 10:31:50 +02:00
< / div >
2026-05-28 08:59:28 +02:00
<!-- Shared asset/logger/position menus (injected by /measurement/menu.js) -->
2025-06-25 10:43:15 +02:00
< div id = "asset-fields-placeholder" > < / div >
< div id = "logger-fields-placeholder" > < / div >
< 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 >