# Reference — Contracts ![code-ref](https://img.shields.io/badge/code--ref-b884c0f-blue) > [!NOTE] > Full topic contract, configuration schema, and child-registration handshake for `measurement`. Source of truth: `src/commands/index.js`, `src/commands/handlers.js`, `src/specificClass.js` `configure()`, and the schema at `generalFunctions/src/configs/measurement.json`. > > Pending full node review (2026-05). Hand-written best-effort placeholder where indicated. For an intuitive overview, return to [Home](Home). --- ## Topic contract The registry lives in `src/commands/index.js`. Each descriptor maps a canonical `msg.topic` to a handler; aliases emit a one-time deprecation warning the first time they fire. | 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). | ### Payload-shape rules | Mode | Accepted | Rejected (logs warn) | |:---|:---|:---| | `analog` | `number`; numeric string (trimmed, non-empty, parses with `Number`) | object payload (hint: "Switch Input Mode to 'digital' …"); non-numeric string | | `digital` | object `{ key1: number, key2: number, … }` — keys must match `config.channels[*].key` | number (hint: "Switch Input Mode to 'analog' …"); array; any non-object | Unknown channel keys in a digital payload are collected and reported at `debug` level via `digital payload contained unmapped keys: `. ### Source / mode allow-lists > [!NOTE] > TODO: `measurement` does not appear to implement a `flowController`-style action/source allow-list (consult `src/specificClass.js`); it relies on the topic registry's typeof checks. If a future hardening pass adds mode-source gating, fold the table in here. --- ## Data model — `getOutput()` shape Source: `src/specificClass.js` `getOutput()` / `getDigitalOutput()` and `src/channel.js` `getOutput()`. Delta-compressed by `outputUtils.formatMsg`: consumers see only the keys that changed. ### Analog mode (`Measurement.getOutput()`) | Key | Type | Unit | Notes | |:---|:---|:---|:---| | `mAbs` | number | scaling units (`asset.unit` / `general.unit`) | Latest output value after offset + scaling + smoothing. Rounded to 2 dp. | | `mPercent` | number | % | Output mapped to `interpolation.percentMin..percentMax`. Rounded to 2 dp. | | `totalMinValue` | number | scaling units | Rolling minimum of the **post-offset, pre-smoothing** values. Reported as `0` until the first sample. | | `totalMaxValue` | number | scaling units | Rolling maximum of the same. Reported as `0` until the first sample. | | `totalMinSmooth` | number | scaling units | Rolling minimum of the smoothed output. Starts at `0`. | | `totalMaxSmooth` | number | scaling units | Rolling maximum of the smoothed output. Starts at `0`. | ### Digital mode (`Measurement.getDigitalOutput()`) ```jsonc { "channels": { "": { "key": "", "type": "", "position": "", "unit": "", "mAbs": , "mPercent": , "totalMinValue": , "totalMaxValue": , "totalMinSmooth": , "totalMaxSmooth": } // ... one entry per channel that has produced output } } ``` ### Status badge `Measurement.getStatusBadge()`: | Mode | Badge text | Fill / shape | |:---|:---|:---| | `analog` | ` ` (e.g. `0.42 bar`) | green / dot | | `digital` | `digital · channel(s)` | blue / ring | The legacy `source.emitter` fires `'mAbs'` (analog only) and is kept for the editor status badge during the refactor window — see [Limitations](Reference-Limitations#legacy-source-emitter). --- ## Events emitted on `source.measurements.emitter` The shared `MeasurementContainer` fires `.measured.` whenever a `Channel`'s rounded output changes. The type / position come from: - **analog**: `config.asset.type` and `config.functionality.positionVsParent`. - **digital**: per-channel `config.channels[i].type` and `config.channels[i].position` (falls back to the node-level `positionVsParent` when missing). Position labels are normalised to **lowercase** in the event name (`upstream`, `downstream`, `atequipment`). Examples: - `pressure.measured.upstream` - `flow.measured.atequipment` - `level.measured.downstream` - `temperature.measured.atequipment` Parents subscribe through the generic `child.measurements.emitter.on(eventName, …)` handshake established by `childRegistrationUtils` (in `generalFunctions`). In digital mode one input message can fan out into several events — one per channel that accepted a value on that tick. --- ## Configuration schema — editor form to config keys Source of truth: `generalFunctions/src/configs/measurement.json` plus `nodeClass.buildDomainConfig`. Defaults below come from the schema. ### General (`config.general`) | Form field | Config key | Default | Notes | |:---|:---|:---|:---| | Name | `general.name` | `Sensor` | Human-readable label. | | (auto-assigned) | `general.id` | `null` | Node-RED node id. | | Default unit | `general.unit` | `unitless` | Falls back to the asset unit. | | Enable logging | `general.logging.enabled` | `true` | Master switch. | | Log level | `general.logging.logLevel` | `info` | `debug` / `info` / `warn` / `error`. | ### Functionality (`config.functionality`) | Form field | Config key | Default | Notes | |:---|:---|:---|:---| | Software type | `functionality.softwareType` | `measurement` | Constant. | | Role | `functionality.role` | `Sensor` | Constant. | | Position vs parent | `functionality.positionVsParent` | `atEquipment` | One of `atEquipment` / `upstream` / `downstream`. Used in the `child.register` payload and as the suffix of the measurement event name. | | Distance offset | `functionality.distance` | `null` | Optional spatial offset; sent with `child.register`. | ### Asset (`config.asset`) | Form field | Config key | Default | Notes | |:---|:---|:---|:---| | Asset UUID | `asset.uuid` | `null` | Globally-unique identifier. | | Tag code / number | `asset.tagCode` / `asset.tagNumber` | `null` | Asset-registry identifiers. | | Geolocation | `asset.geoLocation` | `{x:0, y:0, z:0}` | | | Supplier | `asset.supplier` | `Unknown` | Free text. | | Category | `asset.category` | `sensor` | `sensor` / `measurement`. | | Asset type | `asset.type` | `pressure` | **Required.** Matches the type axis on `MeasurementContainer` and the parent's filter (e.g. `flow`, `power`, `temperature`). | | Model | `asset.model` | `Unknown` | Free text. | | Asset unit | `asset.unit` | `unitless` | Output unit label for the measurement event payload. | | Accuracy | `asset.accuracy` | `null` | Optional sensor accuracy. | | Repeatability | `asset.repeatability` | `null` | Optional repeatability metric. | > [!IMPORTANT] > `asset.type` must match the **exact** string the parent listens for. The parent's filter is typically the bare type (`flow`, `pressure`, `power`, …) — a measurement configured as `flow-electromagnetic` will not register with a `flow`-only filter on its parent (see [Limitations](Reference-Limitations#asset-type-must-match-the-parents-filter-exactly)). ### Scaling (`config.scaling`) | Form field | Config key | Default | Notes | |:---|:---|:---|:---| | Scaling enabled | `scaling.enabled` | `false` | When false, the input is passed through with only the offset applied. | | Input min/max | `scaling.inputMin` / `scaling.inputMax` | `0` / `1` | Source range; clamps the input before mapping. | | Output min/max | `scaling.absMin` / `scaling.absMax` | `50` / `100` | Target range. | | Offset | `scaling.offset` | `0` | Added before scaling; mutated by `cmd.calibrate`. | ### Smoothing (`config.smoothing`) | Form field | Config key | Default | Notes | |:---|:---|:---|:---| | Window size | `smoothing.smoothWindow` | `10` | `>= 1`. Rolling buffer length. | | Method | `smoothing.smoothMethod` | `mean` | One of `none` / `mean` / `min` / `max` / `sd` / `median` / `weightedMovingAverage` / `lowPass` / `highPass` / `bandPass` / `kalman` / `savitzkyGolay`. | ### Outlier detection (`config.outlierDetection`) | Form field | Config key | Default | Notes | |:---|:---|:---|:---| | Enabled | `outlierDetection.enabled` | `false` | Toggle with `set.outlier-detection`. | | Method | `outlierDetection.method` | `zScore` | One of `zScore` / `iqr` / `modifiedZScore`. | | Threshold | `outlierDetection.threshold` | `3` | Method-specific (e.g. z > 3, mz > 3.5). | ### Simulation (`config.simulation`) | Form field | Config key | Default | Notes | |:---|:---|:---|:---| | Enabled | `simulation.enabled` | `false` | When true, `tick()` (1000 ms) drives `inputValue` via `Simulator.step()`. | | Safe calibration time | `simulation.safeCalibrationTime` | `100` | ms before calibration is finalised in sim mode. | ### Interpolation (`config.interpolation`) | Form field | Config key | Default | Notes | |:---|:---|:---|:---| | Percent min | `interpolation.percentMin` | `0` | Lower bound of the `mPercent` output. | | Percent max | `interpolation.percentMax` | `100` | Upper bound. | ### Calibration (`config.calibration`) | Form field | Config key | Default | Notes | |:---|:---|:---|:---| | Stability threshold | `calibration.stabilityThreshold` | `0.01` | Absolute stdDev ceiling (in scaling-units) below which the buffer is considered stable. Fits the default `[50,100]` range; tighten / relax for your sensor. | ### Mode (`config.mode`) | Form field | Config key | Default | Notes | |:---|:---|:---|:---| | Input mode | `mode.current` | `analog` | `analog` (one channel, scalar payload) or `digital` (N channels, object payload). | ### Channels (`config.channels[]` — digital only) In digital mode, each entry in `config.channels` defines its own pipeline: | Field | Required | Falls back to | |:---|:---:|:---| | `key` | yes | — (skipped if missing) | | `type` | yes | — (skipped if missing) | | `position` | no | `config.functionality.positionVsParent` → `atEquipment` | | `unit` | no | `config.asset.unit` → `unitless` | | `distance` | no | `config.functionality.distance` → `null` | | `scaling` | no | `{enabled:false, inputMin:0, inputMax:1, absMin:0, absMax:1, offset:0}` | | `smoothing` | no | `config.smoothing` | | `outlierDetection` | no | `config.outlierDetection` | | `interpolation` | no | `config.interpolation` | Invalid entries (missing `key` or `type`) are logged and skipped. An empty `config.channels[]` in digital mode logs `digital mode enabled but config.channels is empty; no channels will be emitted.` ### Asset registration (`config.assetRegistration`) Used by the `/measurement/asset-reg` admin endpoint to register / sync the asset with the upstream asset registry. Not part of the runtime data path. | Form field | Config key | Default | Notes | |:---|:---|:---|:---| | Profile / location / process ids | `assetRegistration.{profileId, locationId, processId}` | `1` | Free integer ids in the asset registry. | | Status | `assetRegistration.status` | `actief` | Lifecycle status. | | Child assets | `assetRegistration.childAssets` | `[]` | List of child asset ids. | ### Output (`config.output`) | Form field | Config key | Default | Notes | |:---|:---|:---|:---| | Process output | `output.process` | `process` | `process` / `json` / `csv`. Port-0 formatter. | | Database output | `output.dbase` | `influxdb` | `influxdb` / `json` / `csv`. Port-1 formatter. | ### Unit policy > [!NOTE] > TODO: `measurement` does not currently declare a `unitPolicy` block on its `BaseDomain` configuration (unlike `rotatingMachine`). The per-channel `unit` is carried verbatim into the `MeasurementContainer` write at `_writeOutput`. If a future hardening pass adds a unit-policy enforcement, add the canonical / output / required-unit table here. See `CONTRACT.md` for the current invariants. --- ## Child registration Source: `src/specificClass.js` `configure` (registers itself via the `BaseDomain` plumbing) and the standard `childRegistrationUtils` handshake in `generalFunctions`. `measurement` does **not accept children**. It only **registers itself** as a child on its upstream parent. | Layer | Direction | Topic / event | Payload | |:---|:---|:---|:---| | Startup (Port 2) | child → parent | `registerChild` | `{topic: 'registerChild', payload: , positionVsParent, distance}` | | Runtime | child → parent | `.measured.` on `child.measurements.emitter` | `{value, timestamp, unit, distance?}` (per `MeasurementContainer.value()`) | | What | softwareType payload | Side-effect on parent | |:---|:---|:---| | Registration | `measurement` | Parent attaches a listener for `.measured.` on the child's `measurements.emitter`. | | Subsequent updates | event on `child.measurements.emitter` | Parent mirrors the value into its own `MeasurementContainer` slot. | Position labels are normalised to **lowercase** in the event name (`upstream`, `downstream`, `atequipment`); the `positionVsParent` field in the register payload is sent as configured (preserves case). --- ## Related pages | Page | Why | |:---|:---| | [Home](Home) | Intuitive overview | | [Reference — Architecture](Reference-Architecture) | Code map + per-`Channel` pipeline + lifecycle | | [Reference — Examples](Reference-Examples) | Shipped flows + debug recipes | | [Reference — Limitations](Reference-Limitations) | Known issues and open questions | | [EVOLV — Topic Conventions](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions) | Platform-wide topic rules | | [EVOLV — Telemetry](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Telemetry) | Port 0 / 1 / 2 InfluxDB layout |