Replaces the prior stub/partial wiki with a Home + Reference-{Architecture,
Contracts,Examples,Limitations} + _Sidebar structure. Topic-contract and
data-model sections wrapped in AUTOGEN markers for the future wiki-gen tool.
Source-vs-spec contradictions surfaced and flagged inline (not silently
fixed). Pending-review notes mark sections that need a full node review.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
164 lines
7.4 KiB
Markdown
164 lines
7.4 KiB
Markdown
# measurement
|
|
|
|
  
|
|
|
|
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`, …) |
|
|
|
|
---
|
|
|
|
## 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 -.->|"<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: `{<channelKey>: 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 `<type>.measured.<position>` 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<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 / …]
|
|
sf --> sm2[update smooth totalMin/Max]
|
|
sm2 --> wo[round + write outputAbs<br/>+ 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)
|