Replaces the agent-written placeholder inside Reference-Contracts.md with the authoritative table generated from src/commands/index.js. Both the BEGIN and END markers are normalized to the canonical form used by `@evolv/wiki-gen`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
280 lines
14 KiB
Markdown
280 lines
14 KiB
Markdown
# Reference — Contracts
|
|
|
|

|
|
|
|
> [!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.
|
|
|
|
<!-- BEGIN AUTOGEN: topic-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). |
|
|
|
|
<!-- END AUTOGEN: topic-contract -->
|
|
|
|
### 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: <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
|
|
}
|
|
}
|
|
```
|
|
|
|
<!-- END AUTOGEN: data-model -->
|
|
|
|
### Status badge
|
|
|
|
`Measurement.getStatusBadge()`:
|
|
|
|
| Mode | Badge text | Fill / shape |
|
|
|:---|:---|:---|
|
|
| `analog` | `<mAbs> <unit>` (e.g. `0.42 bar`) | green / dot |
|
|
| `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. |
|
|
| (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: <node.id>, positionVsParent, distance}` |
|
|
| 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).
|
|
|
|
---
|
|
|
|
## 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 |
|