Files
measurement/wiki/Home.md
znetsixe 125f964d31 P11.6 wiki regen + Phase 10 private-test rewrites where applicable
For all 11 nodes with auto-gen markers: wiki/Home.md sections 5 (topic
contract) and 9 (data model) regenerated via npm run wiki:all. New
Unit column shows '<measure> (default <unit>)' for declared topics,
'—' otherwise. Effect column now uses descriptor.description (P11.2
field) overriding the generic per-prefix fallback.

For rotatingMachine + reactor: Phase 10 test rewrites — 3 + 8 files
moved off private nodeClass internals (_attachInputHandler, _commands,
_pendingExtras, _registerChild, _tick, etc.) to the public
BaseNodeAdapter surface (node.handlers.input, node.source.*).
+6 / +7 net new tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 19:44:04 +02:00

12 KiB

measurement

Reflects code as of afc304b · 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.

1. What this node is

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.

2. Position in the platform

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| 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.

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.

4. Code map

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
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.

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.

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]

5. Topic contract

Auto-generated from src/commands/index.js. Do NOT hand-edit between the markers. Re-run npm run wiki:contract.

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).

6. Child registration

measurement does not accept children. It only registers itself as a child on its upstream parent.

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

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.

Key Type Unit Sample
mAbs number 0
mPercent number 0
totalMaxSmooth number 0
totalMaxValue number 0
totalMinSmooth number 0
totalMinValue number 0

Concrete digital sample (when mode='digital'):

{
  "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 }
  }
}

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

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

10. State chart

Skipped — measurement is a pure pipeline. There is no FSM. The only mode switch (analog vs digital) is decided once at configure() time and never transitions thereafter; see section 4 for the static branching diagram.

11. Examples

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

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.

12. 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.

Never ship enableLog: 'debug' in a demo — fills the container log within seconds and obscures real errors.

13. When you would NOT use this node

  • 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.

14. Known limitations / current issues

# 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.