Files
measurement/measurement.html
lzm d7f6613892 refactor(measurement): modularize editor JS
Move inline <script> from measurement.html into 8 modules under
src/editor/. measurement.js adds the static-file routes that serve them
to Node-RED.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 09:10:05 +02:00

655 lines
32 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
| S88-niveau | Primair (blokkleur) | Tekstkleur |
| ---------------------- | ------------------- | ---------- |
| **Area** | `#0f52a5` | wit |
| **Process Cell** | `#0c99d9` | wit |
| **Unit** | `#50a8d9` | zwart |
| **Equipment (Module)** | `#86bbdd` | zwart |
| **Control Module** | `#a9daee` | zwart |
-->
<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>
<script>
RED.nodes.registerType("measurement", {
category: "EVOLV",
color: "#D4A02E",
defaults: {
// Define default properties
name: { value: "" }, // use asset category as name
// Input mode: 'analog' (scalar payload, default) or 'digital' (object payload, many channels)
mode: { value: "analog" },
channels: { value: "[]" },
// Define specific properties (analog-mode pipeline defaults)
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 },
stabilityThreshold: { value: 0.01 },
processOutputFormat: { value: "process" },
dbaseOutputFormat: { value: "influxdb" },
//define asset properties
uuid: { value: "" },
supplier: { value: "" },
category: { value: "" },
assetType: { value: "" },
model: { value: "" },
unit: { value: "" },
assetTagNumber: { value: "" },
//logger properties
enableLog: { value: false },
logLevel: { value: "error" },
//physicalAspect
positionVsParent: { value: "" },
positionIcon: { value: "" },
hasDistance: { value: false },
distance: { value: 0 },
distanceUnit: { value: "m" },
distanceDescription: { value: "" }
},
inputs: 1,
outputs: 3,
inputLabels: ["Input"],
outputLabels: ["process", "dbase", "parent"],
icon: "font-awesome/fa-sliders",
label: function () {
const modeTag = this.mode === 'digital' ? ' [digital]' : '';
return (this.positionIcon || "") + " " + (this.assetType || "Measurement") + modeTag;
},
oneditprepare: function () { window.MeasEditor.oneditprepare.call(this); },
oneditsave: function () { window.MeasEditor.oneditsave.call(this); },
});
</script>
<!-- Main UI -->
<script type="text/html" data-template-name="measurement">
<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 / 420 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>
</div>
<!-- ===================== DIGITAL MODE FIELDS ===================== -->
<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>
</div>
<div class="form-row" id="channels-validation" style="margin-left:105px; font-size:12px;"></div>
</div>
<!-- ===================== ANALOG MODE FIELDS ===================== -->
<div id="analog-only-fields">
<!-- ============================================================ -->
<!-- SCALING -->
<!-- ============================================================ -->
<div class="meas-section">
<h4>Scaling</h4>
<p class="meas-help">
Map the raw input range (e.g. 420 mA, 03000 counts) to a physical
process range (e.g. 010 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>
</div>
<!-- ============================================================ -->
<!-- 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>
</div>
<!-- ============================================================ -->
<!-- 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>
</div>
<!-- ============================================================ -->
<!-- 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>
</div>
</div>
<!-- ================================================================ -->
<!-- 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>
<div class="form-row">
<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>
</select>
</div>
<div class="form-row">
<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>
</div>
</div>
<!-- Shared asset/logger/position menus (injected by /measurement/menu.js) -->
<div id="asset-fields-placeholder"></div>
<div id="logger-fields-placeholder"></div>
<div id="position-fields-placeholder"></div>
</script>
<script type="text/html" data-help-name="measurement">
<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>
</script>