# measurement ![code-ref](https://img.shields.io/badge/code--ref-b884c0f-blue) ![s88](https://img.shields.io/badge/S88-Control_Module-a9daee) ![status](https://img.shields.io/badge/status-under--review-orange) A `measurement` turns a raw sensor signal into a validated, scaled, smoothed reading and re-emits it for any upstream parent. Two modes: **analog** (one channel built from the flat config — classic 4–20 mA / PLC style) and **digital** (one `Channel` per `config.channels[]` entry — MQTT / IoT JSON style). It is a leaf in the S88 hierarchy — no children of its own — and registers itself as a child of any parent that accepts measurements (`rotatingMachine`, `machineGroupControl`, `pumpingStation`, `reactor`, `monster`, …). > [!NOTE] > Pending full node review (2026-05). Content reflects `CONTRACT.md`, `src/commands/index.js`, and current source only. Some sections are best-effort placeholders pending the next pass. --- ## At a glance | Thing | Value | |:---|:---| | What it represents | One sensor signal — pressure / flow / power / temperature / level / … | | S88 level | Control Module | | Use it when | You need to scale, offset, smooth, outlier-filter, or simulate a sensor reading before handing it to an equipment / unit / process-cell node | | Don't use it for | Sensor fusion, threshold-trip alarms, or as a control output — this node is read-only signal conditioning | | Children it accepts | None — leaf node | | Parents it talks to | Any node that subscribes to `.measured.` events (`rotatingMachine`, `MGC`, `pumpingStation`, `reactor`, `monster`, …) | --- ## How it fits ```mermaid flowchart LR raw[Raw sensor / MQTT / inject
analog scalar or digital object] m[measurement
Control Module]:::ctrl p1[rotatingMachine
Equipment]:::equip p2[machineGroupControl
Unit]:::unit p3[pumpingStation
Process Cell]:::pc raw -->|data.measurement| m m -->|child.register
(Port 2 at startup)| p1 m -->|child.register| p2 m -->|child.register| p3 m -.->|"<type>.measured.<position>"| p1 m -.->|"<type>.measured.<position>"| p2 m -.->|"<type>.measured.<position>"| p3 classDef pc fill:#0c99d9,color:#fff classDef unit fill:#50a8d9,color:#000 classDef equip fill:#86bbdd,color:#000 classDef ctrl fill:#a9daee,color:#000 ``` S88 colours: Control Module `#a9daee`, Equipment `#86bbdd`, Unit `#50a8d9`, Process Cell `#0c99d9`. Source of truth: `.claude/rules/node-red-flow-layout.md`. --- ## Try it — 1-minute demo Import the basic example flow, deploy, and drive a single sensor through scaling + smoothing. ```bash curl -X POST -H 'Content-Type: application/json' \ --data @nodes/measurement/examples/basic.flow.json \ http://localhost:1880/flows ``` What to do after deploy: 1. Click the `measurement 42` inject — sends `topic: 'measurement'` (legacy alias of `data.measurement`) with payload `42`. 2. Watch Port 0 in the debug pane: `mAbs` updates immediately. After a few injects `totalMinValue` / `totalMaxValue` start tracking the rolling min/max. 3. Toggle the simulator: send `topic: 'set.simulator'`. `tick()` (1000 ms) starts driving `inputValue` through `Simulator.step()`. 4. Trigger calibration: send `topic: 'cmd.calibrate'`. If the rolling window is stable (`stdDev <= config.calibration.stabilityThreshold`) the calibrator captures the current output as the new `config.scaling.offset`. > [!IMPORTANT] > **GIF needed.** Demo recording of steps 1–4 with the live status badge. Save as `wiki/_partial-gifs/measurement/01-basic-demo.gif`, target ≤ 1 MB after `gifsicle -O3 --lossy=80`. --- ## The four things you'll send | Topic | Aliases | Payload | What it does | |:---|:---|:---|:---| | `data.measurement` | `measurement` | analog: `number` (or numeric string); digital: `{: number, …}` | Push a raw reading into the pipeline. Wrong shape for the configured mode logs a hint suggesting the other mode. | | `set.simulator` | `simulator` | (ignored) | Toggle the built-in `Simulator` random-walk on / off. Mutates `config.simulation.enabled`. | | `set.outlier-detection` | `outlierDetection` | (ignored) | Toggle outlier detection on the analog pipeline. Mutates `config.outlierDetection.enabled`. | | `cmd.calibrate` | `calibrate` | (ignored) | Run a one-shot calibration. Captures the current output as `config.scaling.offset`; aborts with a warn if the buffer is not stable. | Aliases log a one-time deprecation warning the first time they fire. --- ## What you'll see come out Sample Port 0 message (analog mode, after a few injects): ```json { "topic": "measurement#sensor_a", "payload": { "mAbs": 0.42, "mPercent": 42, "totalMinValue": 0.12, "totalMaxValue": 0.78, "totalMinSmooth": 0.20, "totalMaxSmooth": 0.65 } } ``` Sample Port 0 message (digital mode): ```json { "topic": "measurement#multi", "payload": { "channels": { "level-a": { "mAbs": 1.84, "mPercent": 73.6, "totalMinValue": 0.1, "totalMaxValue": 2.4 }, "temp-a": { "mAbs": 18.2, "mPercent": 36.4, "totalMinValue": 14.0, "totalMaxValue": 22.1 } } } } ``` | Field | Meaning | |:---|:---| | `mAbs` | Latest output value in scaling-units (after offset + scaling + smoothing). | | `mPercent` | Same value mapped to `interpolation.percentMin..percentMax` (default 0..100). | | `totalMinValue` / `totalMaxValue` | Rolling min/max of **raw** (pre-scaling) values. `0` until first sample. | | `totalMinSmooth` / `totalMaxSmooth` | Rolling min/max of the smoothed output. | Additionally the `source.measurements.emitter` fires `.measured.` on every accepted update — parents subscribe to that event through the `child.measurements.emitter` handshake established at register time. See [Architecture — Lifecycle](Reference-Architecture#lifecycle) for the full path. --- ## How the pipeline behaves ```mermaid flowchart LR in[input value] --> out{outlierDetection.enabled?} out -- yes --> oc[_isOutlier] oc -- outlier --> drop[drop + warn] oc -- ok --> off[apply scaling.offset] out -- no --> off off --> mm[update raw totalMin/Max] mm --> sc{scaling.enabled?} sc -- yes --> lin[linear map
input range → abs range] sc -- no --> sm[pass-through] lin --> sm sm --> sw[push to storedValues
length capped by smoothWindow] sw --> sf[smoothMethod:
mean / median / kalman / …] sf --> sm2[update smooth totalMin/Max] sm2 --> wo[round + write outputAbs
+ emit measurement event] ``` The same pipeline runs per `Channel` instance — once in analog mode, N times in digital mode. --- ## Need more? | Page | What you'll find | |:---|:---| | [Reference — Contracts](Reference-Contracts) | Full topic contract, config schema, child-registration handshake | | [Reference — Architecture](Reference-Architecture) | Code map, lifecycle, analog vs digital branching, per-Channel pipeline | | [Reference — Examples](Reference-Examples) | Shipped example flows + debug recipes | | [Reference — Limitations](Reference-Limitations) | When not to use, known limitations, open questions | [EVOLV master wiki](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Home) · [Topology Patterns](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topology-Patterns) · [Topic Conventions](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions)