> 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.
| `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: <list>`.
### 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.
<!-- BEGIN AUTOGEN: data-model — populate via wiki-gen tool (TODO) -->
### 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": {
"<channel.key>": {
"key": "<channel.key>",
"type": "<channel.type>",
"position": "<channel.position>",
"unit": "<channel.unit>",
"mAbs": <number>,
"mPercent": <number>,
"totalMinValue": <number>,
"totalMaxValue": <number>,
"totalMinSmooth": <number>,
"totalMaxSmooth": <number>
}
// ... one entry per channel that has produced output
| `digital` | `digital · <N> 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 `<type>.measured.<position>` 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. |
| 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 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. |
> `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. |
| 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. |
| `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.`
> 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.
| Runtime | child → parent | `<asset.type>.measured.<positionVsParent>` 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 `<asset.type>.measured.<positionVsParent>` 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).