Consumer half of the abort-token mechanism added in generalFunctions
state.js. executeSequence captures host.state.sequenceAbortToken at
entry, then re-checks before every state transition and after the
optional ramp-down. If MGC (or any external caller) bumps the token
mid-sequence, the loop bails out cleanly — no more barge-through where
a pre-empted shutdown advances through stopping → coolingdown after a
fresh demand has already engaged the pump.
Without this the MGC rendezvous planner can't reliably re-dispatch a
pump that's mid-shutdown: the new flowmovement claims the gate, but
the old shutdown's for-loop keeps running on microtasks and steps the
FSM into idle/off underneath it.
Also: wiki regen following the same visual-first 14-section template as
the other EVOLV nodes — Reference-{Architecture,Contracts,Examples,
Limitations}.md split with _Sidebar.md index.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
280 lines
16 KiB
Markdown
280 lines
16 KiB
Markdown
# Reference — Contracts
|
|
|
|

|
|
|
|
> [!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:
|
|
|
|
```
|
|
<type>.<variant>.<position>.<childId>
|
|
```
|
|
|
|
Position labels are normalised to lowercase in the keys (`atequipment`, `downstream`, `upstream`, `max`, `min`). The trailing `<childId>` is:
|
|
|
|
| `<childId>` | 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`:
|
|
|
|
```
|
|
<mode>: <state-symbol> <ctrl%>% 💨<flow><unit> ⚡<power>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 `'<mode>: 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: `<softwareType>_<id>` | 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.<mode>` | see [Architecture](Reference-Architecture#mode--source-allow-lists) | enforced by `flowController.handle` |
|
|
| (defaults) | `mode.allowedSources.<mode>` | 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 `<type>.measured.<position>` 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 |
|