Auto-generated topic-contract + data-model sections via shared wikiGen script. Hand-written Mermaid diagrams for position-in-platform, code map, child registration, lifecycle, configuration, state chart (where applicable). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
12 KiB
measurement
Reflects code as of
afc304b· regenerated2026-05-11vianpm run wiki:allIf 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-runnpm run wiki:contract.
| Canonical topic | Aliases | Payload | Effect |
|---|---|---|---|
set.simulator |
simulator |
any |
Replaces the named state value with the supplied payload. |
set.outlier-detection |
outlierDetection |
any |
Replaces the named state value with the supplied payload. |
cmd.calibrate |
calibrate |
any |
Triggers an action / sequence — not idempotent. |
data.measurement |
measurement |
any |
Pushes a value into the node's measurement stream. |
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 -.->|"<type>.measured.<position><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/valvefor 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. |