Files
measurement/wiki/Home.md

164 lines
7.4 KiB
Markdown
Raw Normal View History

# 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 `<type>.measured.<position>` events (`rotatingMachine`, `MGC`, `pumpingStation`, `reactor`, `monster`, &hellip;) |
---
## How it fits
```mermaid
flowchart LR
raw[Raw sensor / MQTT / inject<br/>analog scalar or digital object]
m[measurement<br/>Control Module]:::ctrl
p1[rotatingMachine<br/>Equipment]:::equip
p2[machineGroupControl<br/>Unit]:::unit
p3[pumpingStation<br/>Process Cell]:::pc
raw -->|data.measurement| m
m -->|child.register<br/>(Port 2 at startup)| p1
m -->|child.register| p2
m -->|child.register| p3
m -.->|"&lt;type&gt;.measured.&lt;position&gt;"| p1
m -.->|"&lt;type&gt;.measured.&lt;position&gt;"| p2
m -.->|"&lt;type&gt;.measured.&lt;position&gt;"| 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 &mdash; 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 &mdash; 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&ndash;4 with the live status badge. Save as `wiki/_partial-gifs/measurement/01-basic-demo.gif`, target &le; 1&nbsp;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: `{<channelKey>: number, &hellip;}` | 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 `<type>.measured.<position>` on every accepted update &mdash; parents subscribe to that event through the `child.measurements.emitter` handshake established at register time. See [Architecture &mdash; 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<br/>input range → abs range]
sc -- no --> sm[pass-through]
lin --> sm
sm --> sw[push to storedValues<br/>length capped by smoothWindow]
sw --> sf[smoothMethod:<br/>mean / median / kalman / &hellip;]
sf --> sm2[update smooth totalMin/Max]
sm2 --> wo[round + write outputAbs<br/>+ emit measurement event]
```
The same pipeline runs per `Channel` instance &mdash; once in analog mode, N times in digital mode.
---
## Need more?
| Page | What you'll find |
|:---|:---|
| [Reference &mdash; Contracts](Reference-Contracts) | Full topic contract, config schema, child-registration handshake |
| [Reference &mdash; Architecture](Reference-Architecture) | Code map, lifecycle, analog vs digital branching, per-Channel pipeline |
| [Reference &mdash; Examples](Reference-Examples) | Shipped example flows + debug recipes |
| [Reference &mdash; Limitations](Reference-Limitations) | When not to use, known limitations, open questions |
[EVOLV master wiki](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Home) &middot; [Topology Patterns](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topology-Patterns) &middot; [Topic Conventions](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions)