docs(wiki): full 5-page wiki matching the rotatingMachine reference format
Replaces the prior stub/partial wiki with a Home + Reference-{Architecture,
Contracts,Examples,Limitations} + _Sidebar structure. Topic-contract and
data-model sections wrapped in AUTOGEN markers for the future wiki-gen tool.
Source-vs-spec contradictions surfaced and flagged inline (not silently
fixed). Pending-review notes mark sections that need a full node review.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
279
wiki/Reference-Contracts.md
Normal file
279
wiki/Reference-Contracts.md
Normal file
@@ -0,0 +1,279 @@
|
||||
# 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 — populate via wiki-gen tool (TODO) -->
|
||||
|
||||
| Canonical topic | Aliases | Payload | Unit | Effect |
|
||||
|:---|:---|:---|:---|:---|
|
||||
| `set.simulator` | `simulator` | (ignored) | — | Toggles `source.toggleSimulation()` — flips `config.simulation.enabled`. |
|
||||
| `set.outlier-detection` | `outlierDetection` | (ignored) | — | Toggles `source.toggleOutlierDetection()` — flips `config.outlierDetection.enabled` and propagates the new value to `analogChannel.outlierDetection.enabled`. |
|
||||
| `cmd.calibrate` | `calibrate` | (ignored) | — | Calls `source.calibrate()` — if the rolling window is stable, captures the current output as the new `config.scaling.offset`. Aborts with a warn when unstable or when the calibration baseline is missing. |
|
||||
| `data.measurement` | `measurement` | mode-dependent (see below) | per channel (configured) | Push a raw sensor reading into the pipeline. Mode-dispatched in `handlers.dataMeasurement`: **analog** expects a number / numeric string → `source.inputValue = parsed`; **digital** expects an object keyed by channel name → `source.handleDigitalPayload(payload)`. Wrong shape for the configured mode logs a hint suggesting the other mode. |
|
||||
|
||||
<!-- 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 |
|
||||
Reference in New Issue
Block a user