# Reference — Contracts ![code-ref](https://img.shields.io/badge/code--ref-394a972-blue) > [!NOTE] > Full topic contract, configuration schema, and child-registration filters for `rotatingMachine`. Source of truth: `src/commands/index.js`, `src/specificClass.js` `configure()`, and the schema at `generalFunctions/src/configs/rotatingMachine.json`. > > For an intuitive overview, return to the [Home](Home). --- ## Topic contract The registry lives in `src/commands/index.js`. Each descriptor maps a canonical `msg.topic` to its handler; aliases emit a one-time deprecation warning the first time they fire. | Canonical topic | Aliases | Payload | Unit | Effect | |:---|:---|:---|:---|:---| | `set.mode` | `setMode` | `string` (`auto` / `virtualControl` / `fysicalControl`) | — | Switch operational mode. Each mode has its own allow-list of actions and sources. | | `cmd.startup` | — | any | — | Run the configured `startup` sequence (default `[starting, warmingup, operational]`). | | `cmd.shutdown` | — | any | — | Run the `shutdown` sequence. If currently `operational`, `executeSequence` first ramps the setpoint to 0 (interruptible). | | `cmd.estop` | `emergencystop` | any | — | Run the `emergencystop` sequence (default `[emergencystop, off]`). Reachable from every state. | | `set.setpoint` | `execMovement` | `{setpoint: number}` | control % (no `units` — convert has no `percent` measure) | Move to a control-axis setpoint via `state.moveTo`. | | `set.flow-setpoint` | `flowMovement` | `{setpoint: number}` or bare number | `volumeFlowRate` (default `m3/h`) | Convert to canonical m³/s, then to control % via `predictCtrl.y`, then `state.moveTo`. | | `data.simulate-measurement` | `simulateMeasurement` | `{asset: {type, unit}, value, position, childName?, childId?}` | type-specific | Inject a virtual sensor reading. The two virtual children (`dashboard-sim-upstream` / `-downstream`) auto-handle pressure; other types use the registering child's id. | | `query.curves` | `showWorkingCurves` | any | — | Reply on Port 0 with the current working curves (flow / power / efficiency). | | `query.cog` | `CoG` | any | — | Reply on Port 0 with the centre-of-gravity (CoG) point. | | `child.register` | `registerChild` | `string` (child node id) | — | Register a `measurement` child with this machine. Port 2 wiring does this automatically in normal flows. | | `execSequence` | — | `{action: "startup" \| "shutdown"}` | — | Legacy umbrella: demuxes `payload.action` to the canonical `cmd.startup` / `cmd.shutdown` handler. Marked `_legacy: true`; scheduled for removal. | ### Mode / source / action allow-lists A topic that survives the registry still passes through `flowController.handle`: ```js if (!host.isValidActionForMode(action, host.currentMode)) return; if (!host.isValidSourceForMode(source, host.currentMode)) return; ``` Defaults from the schema: | Mode | `allowedActions` | `allowedSources` | |:---|:---|:---| | `auto` | `statuscheck, execmovement, execsequence, flowmovement, emergencystop, entermaintenance` | `parent, GUI, fysical` | | `virtualControl` | `statuscheck, execmovement, flowmovement, execsequence, emergencystop, exitmaintenance` | `GUI, fysical` | | `fysicalControl` | `statuscheck, emergencystop, entermaintenance, exitmaintenance` | `fysical` | A rejected request logs at warn and short-circuits; nothing reaches the FSM. --- ## Data model — `getOutput()` shape Composed each tick by `src/io/output.js` `buildOutput()`. Delta-compressed: consumers see only the keys that changed. ### Per-measurement keys For every `(type, variant, position)` stored in MeasurementContainer, the flattened output emits: ``` ... ``` Position labels are normalised to lowercase in the keys (`atequipment`, `downstream`, `upstream`, `max`, `min`). The trailing `` is: | `` | When | |:---|:---| | `default` | The node's own predictions (flow / power / efficiency / Ncog). | | `dashboard-sim-upstream` / `dashboard-sim-downstream` | The two auto-registered virtual pressure children. | | The real child's `general.id` | When a registered measurement child wrote the value. | Sample keys (operational pump, simulated pressure): | Key | Type | Unit | Notes | |:---|:---|:---|:---| | `flow.predicted.downstream.default` | number | m³/h | Live predicted flow. | | `flow.predicted.atequipment.default` | number | m³/h | Same number, equipment-side label. | | `flow.predicted.max.default` / `.min.default` | number | m³/h | Curve envelope at the current `fDimension`. | | `power.predicted.atequipment.default` | number | kW | Predicted shaft power. | | `pressure.measured.upstream.dashboard-sim-upstream` | number | mbar | Last simulated suction pressure. | | `pressure.measured.downstream.dashboard-sim-downstream` | number | mbar | Last simulated discharge pressure. | | `temperature.measured.atequipment.dashboard-sim-upstream` | number | °C | Default 15°C until overwritten. | | `atmPressure.measured.atequipment.dashboard-sim-upstream` | number | Pa | Default 101325 Pa until overwritten. | ### Scalar keys | Key | Type | Source | Notes | |:---|:---|:---|:---| | `state` | string | `host.state.getCurrentState()` | One of the FSM states (`idle`, `starting`, `warmingup`, …). | | `ctrl` | number | `host.state.getCurrentPosition()` | Control-axis position 0..100. | | `mode` | string | `host.currentMode` | `auto` / `virtualControl` / `fysicalControl`. | | `runtime` | number | `host.state.getRunTimeHours()` | Cumulative hours in active states. | | `moveTimeleft` | number | `host.state.getMoveTimeLeft()` | Seconds remaining on the current move (0 when idle). | | `maintenanceTime` | number | `host.state.getMaintenanceTimeHours()` | Cumulative hours in maintenance. | | `cog` / `NCog` / `NCogPercent` | number | `host.cog` etc. | CoG metric on the η curve. `NCog` 0..1; `NCogPercent` is `NCog * 100`, rounded to 2 dp. | | `effDistFromPeak` | number | `host.absDistFromPeak` | Absolute η distance to peak. | | `effRelDistFromPeak` | number | `host.relDistFromPeak` | Normalised 0..1; `undefined` when η band collapses. | | `predictionQuality` | string | `host.predictionHealth.quality` | `good` / `warming` / `degraded` / `invalid`. | | `predictionConfidence` | number | `host.predictionHealth.confidence` | 0..1, rounded to 3 dp. | | `predictionPressureSource` | string \| null | `host.predictionHealth.pressureSource` | `dashboard-sim` or a real child id; null until pressure landed. | | `predictionFlags` | array | `host.predictionHealth.flags` | Reason codes (e.g. `pressure_init_warming`). | | `pressureDriftLevel` | number | `host.pressureDrift.level` | 0..3. | | `pressureDriftSource` | string \| null | `host.pressureDrift.source` | Source whose drift is worst. | | `pressureDriftFlags` | array | `host.pressureDrift.flags` | `nominal` when no drift detected. | | `flowNrmse` / `flowLongTermNRMSD` / `flowImmediateLevel` / `flowLongTermLevel` / `flowDriftValid` | numbers / number / number / boolean | `host.flowDrift` | Only present once `flowDrift != null`. | | `powerNrmse` / `powerLongTermNRMSD` / `powerImmediateLevel` / `powerLongTermLevel` / `powerDriftValid` | same | `host.powerDrift` | Same. | ### Status badge `buildStatusBadge` in `io/output.js`: ``` : % 💨kW ``` State symbols (per `STATE_SYMBOLS` map): | State | Symbol | Fill | |:---|:---:|:---| | `off` | ⬛ | red | | `idle` | ⏸️ | blue | | `operational` | ⏵️ | green | | `starting` | ⏯️ | yellow | | `warmingup` | 🔄 | green | | `accelerating` | ⏩ | yellow | | `decelerating` | ⏪ | yellow | | `stopping` | ⏹️ | yellow | | `coolingdown` | ❄️ | yellow | | `maintenance` | 🔧 | grey | Pressure-not-initialised states (`operational`, `warmingup`, `accelerating`, `decelerating`) override the badge to a yellow ring `': pressure not initialized'` until at least one pressure source has been written. --- ## Configuration schema — editor form to config keys Source of truth: `generalFunctions/src/configs/rotatingMachine.json` plus `nodeClass.buildDomainConfig`. ### General (`config.general`) | Form field | Config key | Default | Notes | |:---|:---|:---|:---| | Name | `general.name` | derived: `_` | Re-derived in `configure()`. | | (auto-assigned) | `general.id` | `null` | Node-RED node id. | | Default unit | `general.unit` | `l/s` (schema) / `m3/h` (nodeClass) | `buildDomainConfig` resolves `uiConfig.unit` via `convert` and overrides to a valid flow 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 | |:---|:---|:---|:---| | Position vs parent | `functionality.positionVsParent` | `atEquipment` | One of `atEquipment` / `upstream` / `downstream`. Used in the child-register payload that goes UP to MGC / pumpingStation. | | (hidden) | `functionality.softwareType` | `rotatingmachine` | Constant. | | (hidden) | `functionality.role` | `RotationalDeviceController` | Constant. | | Distance offset | `functionality.distance` | `null` | Optional spatial offset; populated when `hasDistance` is enabled. | | Distance unit | `functionality.distanceUnit` | `m` | | | Distance description | `functionality.distanceDescription` | `""` | Free-text. | ### Asset (`config.asset`) Resolved derived metadata (supplier / category / type / allowed units) lives in `generalFunctions/datasets/assetData/rotatingmachine.json` keyed by `asset.model`. The editor's asset menu reads from that registry. | Form field | Config key | Default | Notes | |:---|:---|:---|:---| | Asset UUID | `asset.uuid` | `null` | Globally-unique identifier. | | Tag code | `asset.tagCode` | `null` | | | Tag number | `asset.tagNumber` | `null` | Legacy column. | | Geolocation | `asset.geoLocation` | `{x:0, y:0, z:0}` | | | Model | `asset.model` | `null` | **Required.** Resolves curve + supplier / type / allowed units via the registry. | | Deployment unit | `asset.unit` | `null` | **Required.** Must be a flow unit; soft-warned if not in the registry's recommended list for the model. | | Curve units | `asset.curveUnits` | `{pressure:'mbar', flow:'m3/h', power:'kW', control:'%'}` | Carried for curve normalisation. | | Accuracy | `asset.accuracy` | `null` | Optional sensor accuracy %. | | (derived) | `asset.machineCurve` | `{nq:{}, np:{}}` | Loaded from `loadModelCurve(model)`, then normalised. | > [!WARNING] > **Legacy fields removed.** `supplier`, `category`, and `assetType` are no longer node config — the registry derives them from the model. Flows saved before the AssetResolver refactor will throw a startup error with a clear migration message. Re-open the node, re-select the model from the asset menu, and save. ### State times (`stateConfig.time`) Set on the state machine via `nodeClass.buildDomainConfig` from editor fields: | Form field | Config key | Default (schema) | Notes | |:---|:---|:---|:---| | Startup Time | `time.starting` | configured in s | Time spent in `starting` before transitioning to `warmingup`. | | Warmup Time | `time.warmingup` | configured in s | Time in `warmingup` — **non-interruptible** safety. | | Shutdown Time | `time.stopping` | configured in s | Time in `stopping`. | | Cooldown Time | `time.coolingdown` | configured in s | Time in `coolingdown` — **non-interruptible** safety. | ### Movement (`stateConfig.movement`) | Form field | Config key | Default | Notes | |:---|:---|:---|:---| | Reaction Speed | `movement.speed` | configured in %/s | Controller ramp rate. E.g. `1` means 1%/s → setpoint 60 from idle reaches 60 in ~60 s. | | Movement Mode | `movement.mode` | `staticspeed` | `staticspeed` (linear ramp) or `dynspeed` (cubic ease-in-out). Both yield the same total duration; only the curve differs. | | (internal) | `movement.maxSpeed` | from schema | Hard cap honoured by `movementManager.getNormalizedSpeed`. | | (internal) | `movement.interval` | from schema | Inner-loop tick of the move animation (ms). | ### Sequences (`config.sequences`) State-transition lists per sequence name. Defaults: | Sequence | States | |:---|:---| | `startup` | `[starting, warmingup, operational]` | | `shutdown` | `[stopping, coolingdown, idle]` | | `emergencystop` | `[emergencystop, off]` | | `boot` | `[idle, starting, warmingup, operational]` | | `entermaintenance` | `[stopping, coolingdown, idle, maintenance]` | | `exitmaintenance` | `[off, idle]` | Custom sequences are accepted as long as every step is a known FSM state and the transitions between them are allowed by `stateConfig.allowedTransitions`. ### Output (`config.output`) | Form field | Config key | Default | Range | Notes | |:---|:---|:---|:---|:---| | Process Output | `output.process` | `process` | `process` / `json` / `csv` | Port-0 formatter. | | Database Output | `output.dbase` | `influxdb` | `influxdb` / `json` / `csv` | Port-1 formatter. | ### Mode (`config.mode`) | Form field | Config key | Default | Range | Notes | |:---|:---|:---|:---|:---| | Mode | `mode.current` | `auto` | `auto` / `virtualControl` / `fysicalControl` | The active operational mode. | | (defaults) | `mode.allowedActions.` | see [Architecture](Reference-Architecture#mode--source-allow-lists) | enforced by `flowController.handle` | | (defaults) | `mode.allowedSources.` | see [Architecture](Reference-Architecture#mode--source-allow-lists) | enforced by `flowController.handle` | ### Unit policy Source: `src/specificClass.js` lines 36–41. | Quantity | Canonical (internal) | Output (rendered) | Curve (supplier) | Required-unit | |:---|:---|:---|:---|:---:| | Pressure | `Pa` | `mbar` | `mbar` | ✓ | | Atmospheric pressure | `Pa` | `Pa` | — | ✓ | | Flow | `m3/s` | `m3/h` | `m3/h` | ✓ | | Power | `W` | `kW` | `kW` | ✓ | | Temperature | `K` | `°C` | — | ✓ | | Control | — | — | `%` | — | `requireUnitForTypes` means MeasurementContainer rejects writes that omit `unit` for these types. --- ## Child registration Source: `src/measurement/childRegistrar.js` `registerMeasurementChild`. The registrar reads `asset.type` and `positionVsParent` from the child's config and subscribes to `.measured.` on the child's measurement emitter. | Software type | Filter | Wired to | Side-effect | |:---|:---|:---|:---| | `measurement` | `asset.type='pressure', position=upstream` | `pressureRouter.route('upstream', value, ctx)` | Stored as upstream pressure; refresh prediction + drift. `pressureInitialization` tracks readiness. | | `measurement` | `asset.type='pressure', position=downstream` | `pressureRouter.route('downstream', value, ctx)` | Same on the discharge side. | | `measurement` | `asset.type='flow', position=*` | `measurementHandlers.updateMeasuredFlow` | Stored; drift assessed against predicted. | | `measurement` | `asset.type='power', position=atEquipment` | `measurementHandlers.updateMeasuredPower` | Stored; drift assessed against predicted. | | `measurement` | `asset.type='temperature', position=*` | `measurementHandlers.updateMeasuredTemperature` | Stored; surfaced on Port 0. | ### Virtual pressure children — auto-registered At startup `specificClass` registers two `measurement`-typed children: | Child id | Position | Default value | Use | |:---|:---|:---|:---| | `dashboard-sim-upstream` | `upstream` | 0 mbar | Receives `data.simulate-measurement` payloads with position `upstream`. | | `dashboard-sim-downstream` | `downstream` | 0 mbar | Same for `downstream`. | `pressureSelector` prefers a real registered child over the virtuals once one shows up — the virtuals keep listening so dashboards can still inject sim values during real-pressure outages. --- ## Related pages | Page | Why | |:---|:---| | [Home](Home) | Intuitive overview | | [Reference — Architecture](Reference-Architecture) | Code map, FSM, prediction + drift pipeline | | [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 |