docs(wiki): full 5-page wiki matching the rotatingMachine reference format

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>
This commit is contained in:
znetsixe
2026-05-19 09:42:10 +02:00
parent b884c0f085
commit 1a16f9c4f1
6 changed files with 916 additions and 203 deletions

View File

@@ -1,13 +1,28 @@
# measurement
> **Reflects code as of `125f964` · regenerated `2026-05-11` via `npm run wiki:all`**
> If this banner is stale, the page may be out of date. Treat as informative, not authoritative.
![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)
## 1. What this node is
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 &mdash; classic 4&ndash;20&nbsp;mA / PLC style) and **digital** (one `Channel` per `config.channels[]` entry &mdash; MQTT / IoT JSON style). It is a leaf in the S88 hierarchy &mdash; no children of its own &mdash; and registers itself as a child of any parent that accepts measurements (`rotatingMachine`, `machineGroupControl`, `pumpingStation`, `reactor`, `monster`, &hellip;).
**measurement** is an S88 Control Module that turns a raw sensor signal into a validated, scaled, smoothed reading and re-emits it for any parent. Two modes: **analog** (one channel built from the flat config) and **digital** (one Channel per `config.channels[]` entry). It is a leaf in the hierarchy — no children of its own.
> [!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.
## 2. Position in the platform
---
## At a glance
| Thing | Value |
|:---|:---|
| What it represents | One sensor signal &mdash; pressure / flow / power / temperature / level / &hellip; |
| 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 &mdash; this node is read-only signal conditioning |
| Children it accepts | None &mdash; 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
@@ -18,12 +33,12 @@ flowchart LR
p3[pumpingStation<br/>Process Cell]:::pc
raw -->|data.measurement| m
m -->|child.register| p1
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
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
@@ -32,227 +47,117 @@ flowchart LR
S88 colours: Control Module `#a9daee`, Equipment `#86bbdd`, Unit `#50a8d9`, Process Cell `#0c99d9`. Source of truth: `.claude/rules/node-red-flow-layout.md`.
## 3. Capability matrix
---
| Capability | Status | Notes |
|---|---|---|
| Analog mode — single channel from flat config | ✅ | Default. `data.measurement` payload is numeric. |
| Digital mode — many channels from `config.channels[]` | ✅ | Payload is an object keyed by `channel.key`. |
| Outlier detection | ✅ | Median ± window check. Toggleable via `set.outlier-detection`. |
| Scaling (input range → process range + offset) | ✅ | `config.scaling.{inputMin,inputMax,absMin,absMax,offset}`. |
| Smoothing (moving window) | ✅ | `config.smoothing.{smoothWindow,smoothMethod}`. |
| Min/max tracking | ✅ | `totalMinValue`, `totalMaxValue`, smoothed variants. |
| Calibration (capture current as zero/reference) | ✅ | `cmd.calibrate`. Mutates `config.scaling.offset`. |
| Built-in simulator | ✅ | Sinusoidal/noise driver — `set.simulator` toggles. |
| Repeatability / stability metrics | ✅ | `evaluateRepeatability()`, `isStable()`. |
| Accepts children of its own | ❌ | Leaf node. |
## Try it &mdash; 1-minute demo
## 4. Code map
Import the basic example flow, deploy, and drive a single sensor through scaling + smoothing.
```mermaid
flowchart TB
subgraph nodeRED["nodeClass.js — adapter (BaseNodeAdapter)"]
nc["buildDomainConfig()<br/>static DomainClass, commands<br/>static tickInterval = 1000ms"]
end
subgraph domain["specificClass.js — orchestrator (BaseDomain)"]
sc["Measurement.configure()<br/>mode = analog | digital<br/>builds Channel(s)"]
end
subgraph concerns["src/ concern modules"]
channel["channel.js<br/>outlier → offset → scaling →<br/>smoothing → minMax pipeline"]
simulation["simulation/<br/>built-in Simulator"]
calibration["calibration/<br/>Calibrator + stability"]
commands["commands/<br/>topic registry + handlers"]
end
nc --> sc
sc --> channel
sc --> simulation
sc --> calibration
nc --> commands
```bash
curl -X POST -H 'Content-Type: application/json' \
--data @nodes/measurement/examples/basic.flow.json \
http://localhost:1880/flows
```
| Module | Owns | Read first if you're changing… |
|---|---|---|
| `channel.js` | Per-channel pipeline (outlier → offset → scaling → smoothing → emit) | Per-tick reading flow, unit semantics, emitted event name. |
| `simulation/` | Built-in signal generator for demos and offline tests | Sim behaviour, period / amplitude. |
| `calibration/` | Stability checks, repeatability, offset capture | `cmd.calibrate` behaviour, stable-window heuristic. |
| `commands/` | Input-topic registry and handlers | New input topics, payload validation. |
What to do after deploy:
The analog/digital branch is decided once in `configure()` based on `config.mode.current`. There is no FSM — `tick()` only pumps the simulator when enabled.
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`.
```mermaid
flowchart LR
cfg[config.mode.current]
cfg -->|"=== 'digital'"| dig[Build N Channels<br/>from config.channels[]]
cfg -->|"=== 'analog' (default)"| ana[Build 1 Channel<br/>from flat config]
dig --> emit_d[handleDigitalPayload<br/>fan-out per channel]
ana --> emit_a[inputValue setter<br/>single channel update]
```
> [!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`.
## 5. Topic contract
---
> **Auto-generated** from `src/commands/index.js`. Do NOT hand-edit between the markers. Re-run `npm run wiki:contract`.
## The four things you'll send
<!-- BEGIN AUTOGEN: topic-contract -->
| 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. |
| Canonical topic | Aliases | Payload | Unit | Effect |
|---|---|---|---|---|
| `set.simulator` | `simulator` | `any` | — | Toggle the built-in simulator on / off. |
| `set.outlier-detection` | `outlierDetection` | `any` | — | Toggle / configure outlier detection on the measurement pipeline. |
| `cmd.calibrate` | `calibrate` | `any` | — | Trigger a one-shot calibration of the measurement. |
| `data.measurement` | `measurement` | `any` | — | Push a raw measurement (analog: number; digital: per-channel object). |
Aliases log a one-time deprecation warning the first time they fire.
<!-- END AUTOGEN: topic-contract -->
---
## 6. Child registration
## What you'll see come out
`measurement` does **not accept children**. It only **registers itself** as a child on its upstream parent.
Sample Port 0 message (analog mode, after a few injects):
```mermaid
flowchart LR
m[measurement]:::ctrl -->|"child.register<br/>(Port 2 at startup)"| parent[rotatingMachine /<br/>MGC / pumpingStation /<br/>reactor / monster]
m -.->|"&lt;type&gt;.measured.&lt;position&gt;<br/>(measurements.emitter)"| parent
classDef ctrl fill:#a9daee,color:#000
```
| What | softwareType payload | Side-effect on parent |
|---|---|---|
| Registration | `measurement` | Parent attaches listener for `<asset.type>.measured.<positionVsParent>`. |
| Subsequent updates | event on `child.measurements.emitter` | Parent mirrors value into its own `MeasurementContainer`. |
Position labels are normalised to **lowercase** in the event name (`upstream`, `downstream`, `atequipment`).
## 7. Lifecycle — what one event (or tick) does
```mermaid
sequenceDiagram
participant ext as external sender
participant m as measurement
participant ch as Channel pipeline
participant emitter as measurements.emitter
participant parent as parent (e.g. rotatingMachine)
ext->>m: data.measurement (12.4)
m->>m: command dispatch (analog branch)
m->>ch: update(12.4)
ch->>ch: outlier check → offset → scale → smooth → minMax
ch->>emitter: <type>.measured.<position> {value, ts, unit}
emitter-->>parent: child event (subscribed at register-time)
m->>m: notifyOutputChanged()
m-->>ext: Port 0 + Port 1 (delta-compressed)
Note over m: every 1000 ms: if simulation.enabled,<br/>simulator.step() → inputValue
```
## 8. Data model — `getOutput()`
Analog mode emits the legacy scalar shape. Digital mode emits a nested `{channels:{...}}` keyed by `channel.key`.
<!-- BEGIN AUTOGEN: data-model -->
| Key | Type | Unit | Sample |
|---|---|---|---|
| `mAbs` | number | — | `0` |
| `mPercent` | number | — | `0` |
| `totalMaxSmooth` | number | — | `0` |
| `totalMaxValue` | number | — | `0` |
| `totalMinSmooth` | number | — | `0` |
| `totalMinValue` | number | — | `0` |
<!-- END AUTOGEN: data-model -->
**Concrete digital sample** (when `mode='digital'`):
~~~json
```json
{
"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 }
"topic": "measurement#sensor_a",
"payload": {
"mAbs": 0.42,
"mPercent": 42,
"totalMinValue": 0.12,
"totalMaxValue": 0.78,
"totalMinSmooth": 0.20,
"totalMaxSmooth": 0.65
}
}
~~~
In addition, the legacy `source.emitter` fires `'mAbs'` (analog only) — kept for the editor status badge during the refactor window.
## 9. Configuration — editor form ↔ config keys
```mermaid
flowchart TB
subgraph editor["Node-RED editor form"]
f1[Mode: analog / digital]
f2[Asset type + unit]
f3[Position vs parent]
f4[Scaling: inputMin/Max, absMin/Max, offset]
f5[Smoothing: window + method]
f6[Outlier detection: enabled + window]
f7[Simulation: enabled + amplitude/period]
f8[Digital channels list]
end
subgraph cfg["Domain config slice"]
c1[mode.current]
c2[asset.type / asset.unit]
c3[functionality.positionVsParent]
c4[scaling.*]
c5[smoothing.*]
c6[outlierDetection.*]
c7[simulation.*]
c8[channels []]
end
f1 --> c1
f2 --> c2
f3 --> c3
f4 --> c4
f5 --> c5
f6 --> c6
f7 --> c7
f8 --> c8
```
| Form field | Config key | Default | Range | Where used |
|---|---|---|---|---|
| Mode | `mode.current` | `analog` | enum (`analog`, `digital`) | `Measurement.configure` |
| Asset type | `asset.type` | `pressure` | enum | event name + unit policy |
| Position vs parent | `functionality.positionVsParent` | `atEquipment` | enum | event name suffix |
| Scaling enabled | `scaling.enabled` | `false` | bool | `Channel._applyScaling` |
| Input min/max | `scaling.inputMin/Max` | `0` / `1` | numeric | linear map foot/top |
| Output min/max | `scaling.absMin/absMax` | `50` / `100` | numeric | linear map foot/top |
| Offset | `scaling.offset` | `0` | numeric | calibration target |
| Smoothing window | `smoothing.smoothWindow` | `10` | ≥ 1 (samples) | moving window |
| Outlier detection | `outlierDetection.enabled` | varies | bool | `Channel._isOutlier` |
| Simulation enabled | `simulation.enabled` | `false` | bool | `tick()` step |
Sample Port 0 message (digital mode):
## 10. Examples
```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 }
}
}
}
```
| Tier | File | What it shows | Status |
|---|---|---|---|
| Basic | `examples/basic.flow.json` | Inject + dashboard, no parent | ⚠️ legacy shape, pre-refactor |
| Integration | `examples/integration.flow.json` | measurement registered as child of a parent | ⚠️ legacy shape, pre-refactor |
| Edge | `examples/edge.flow.json` | Outlier / scaling / simulator edge cases | ⚠️ legacy shape, pre-refactor |
| 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. |
Tier 1/2/3 visual-first example flows are still TODO (see `MEMORY.md` "TODO: Example Flows"). Screenshots will land under `wiki/_partial-screenshots/measurement/` when the new flows ship.
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.
## 11. Debug recipes
---
| Symptom | First thing to check | Where to look |
|---|---|---|
| Parent never receives `<type>.measured.<position>` | `assetType` must match parent's filter exactly (e.g. `flow` — not `flow-electromagnetic`). | `config.asset.type` + `MEMORY.md` integration gotcha. |
| Position labels look uppercase to parent | Event name lowercases — but `functionality.positionVsParent` is sent as-is on `child.register`. | `_buildAnalogChannel` event-name composition. |
| Outliers seem to pass through | `outlierDetection.enabled` may be off (default varies by config). Toggle with `set.outlier-detection`. | `Channel._isOutlier`. |
| `cmd.calibrate` does nothing | Calibrator requires ≥ 2 stable samples — check `isStable()` first. | `calibration/calibrator.js`. |
| Digital payload silently dropped | Unknown channel keys land in the `unknown` log line only at debug level. | enable `logging.logLevel=debug` momentarily. |
| Simulator still running after toggle off | `tick()` reads `config.simulation.enabled` each tick — confirm the toggle actually mutated the config. | `toggleSimulation`. |
## How the pipeline behaves
> Never ship `enableLog: 'debug'` in a demo — fills the container log within seconds and obscures real errors.
```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]
```
## 12. When you would NOT use this node
The same pipeline runs per `Channel` instance &mdash; once in analog mode, N times in digital mode.
- Don't use measurement to **fuse** signals from multiple sensors — it's per-channel only. Aggregate at the parent.
- Don't use measurement for **control output** — it's read-only signal conditioning. Use `rotatingMachine` / `valve` for actuation.
- Don't use measurement for **alarm logic** — there is no threshold-trip output. Build that on top of the emitted reading at the parent or in a dashboard rule.
---
## 13. Known limitations / current issues
## Need more?
| # | Issue | Tracked in |
|---|---|---|
| 1 | Legacy `source.emitter` 'mAbs' event still fired alongside `measurements.emitter` — slated for removal in Phase 7. | `OPEN_QUESTIONS.md` (2026-05-10) |
| 2 | Digital mode's per-channel scaling/smoothing falls back to the analog block's defaults when not specified per channel. | `_buildDigitalChannels`. |
| 3 | Tier 1/2/3 visual-first example flows not yet written; current `examples/` only contains pre-refactor flows. | P9 / P2.14 follow-up. |
| 4 | No automatic recalibration — `cmd.calibrate` is operator-triggered. | `calibration/calibrator.js`. |
| 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)