# rotatingMachine > **Reflects code as of `1a9f533` · regenerated `2026-05-11` via `npm run wiki:all`** > If this banner is stale, the page may be out of date. Treat as informative, not authoritative. ## 1. What this node is **rotatingMachine** models a single pump, compressor, or blower. It loads a supplier characteristic curve, takes upstream + downstream pressure measurements (or simulated values), predicts the resulting flow + power, drives a startup/shutdown state machine, and assesses prediction drift against measured flow / power. Used as a child of `machineGroupControl` when grouped, or directly under a `pumpingStation`. ## 2. Position in the platform ```mermaid flowchart LR parent[machineGroupControl /
pumpingStation]:::unit -->|flowmovement
execsequence| rm[rotatingMachine
Equipment]:::equip m_up[measurement
pressure upstream]:::ctrl -.data.-> rm m_dn[measurement
pressure downstream]:::ctrl -.data.-> rm sim[dashboard-sim
virtual pressure children]:::ctrl -.data.-> rm rm -->|child.register| parent rm -.->|flow.predicted.*
power.predicted.atequipment| parent classDef unit fill:#50a8d9,color:#000 classDef equip fill:#86bbdd,color:#000 classDef ctrl fill:#a9daee,color:#000 ``` S88 colours: Unit `#50a8d9`, Equipment `#86bbdd`, Control Module `#a9daee`. Source of truth: `.claude/rules/node-red-flow-layout.md`. ## 3. Capability matrix | Capability | Status | Notes | |---|---|---| | Curve-based flow prediction | ✅ | Built from `asset.model` via `curves/curveLoader`. | | Curve-based power prediction | ✅ | Reverse curve composed inside `buildPredictors`. | | FSM (startup / shutdown / movement) | ✅ | Shared `state/state.js` from generalFunctions. | | Interruptible movements | ✅ | `abortMovement` from MGC overrides on new demand. | | Drift assessment (flow + power) | ✅ | `DriftAssessor` with EWMA + alignment tolerance. | | Virtual pressure children for sim | ✅ | `dashboard-sim-upstream / -downstream`. | | Real-pressure child preference | ✅ | `pressureSelector` prefers real over virtual. | | Group operating-point prediction | ✅ | `setGroupOperatingPoint` for MGC integration. | | `cmd.estop` hard cut | ✅ | Forces `emergencystop` state. | | `data.simulate-measurement` injection | ✅ | Pressure / flow / power / temperature. | | Auto-recovery from prediction loss | ⚠️ | Reverts to null predictors silently — health falls to `invalid`. | | Multi-parent registration | ⚠️ | Accepted but not exercised in production. | ## 4. Code map ```mermaid flowchart TB subgraph nodeRED["nodeClass.js — adapter (BaseNodeAdapter)"] nc["buildDomainConfig()
static DomainClass, commands"] end subgraph domain["specificClass.js — orchestrator (BaseDomain)"] sc["Machine.configure()
_setupCurves / _setupState /
_setupDrift / _setupPressure /
_setupChildren"] end subgraph concerns["src/ concern modules"] curves["curves/
loadModelCurve + normalize"] prediction["prediction/
buildPredictors + math"] drift["drift/
DriftAssessor + healthRefresh"] pressure["pressure/
init + router + selector + virtual"] state["state/
FSM bindings + sequenceController"] measurement["measurement/
handlers + childRegistrar"] flow["flow/
flowController (handleInput)"] display["display/
workingCurves + CoG"] io["io/
output + status"] commands["commands/
topic registry + handlers"] end nc --> sc sc --> curves sc --> prediction sc --> drift sc --> pressure sc --> state sc --> measurement sc --> flow sc --> display sc --> io nc --> commands ``` | Module | Owns | Read first if you're changing… | |---|---|---| | `curves/` | Supplier curve loader + normaliser + reverse | Curve fitting, unit mismatches, fallback. | | `prediction/` | Per-machine + group predictors, math helpers | Predicted flow / power values. | | `drift/` | DriftAssessor (EWMA, alignment), healthRefresh | Prediction quality, flags, confidence. | | `pressure/` | init + router + selector + virtual children | Pressure plumbing, sim vs real preference. | | `state/` | FSM bindings + setpoint / sequence orchestration | Startup / shutdown sequences. | | `measurement/` | Measurement handlers + child registrar | Measured value plumbing per type. | | `flow/` | `flowController.handle(source, action, parameter)` | Top-level input dispatch. | | `display/` | `showWorkingCurves`, `showCoG` | `query.curves` / `query.cog` outputs. | | `io/` | `getOutput`, `getStatusBadge` | Output shape, badge text. | | `commands/` | Input-topic registry and handlers | New input topics, payload validation. | ## 5. Topic contract > **Auto-generated** from `src/commands/index.js`. Do NOT hand-edit between the markers. Re-run `npm run wiki:contract`. | Canonical topic | Aliases | Payload | Unit | Effect | |---|---|---|---|---| | `set.mode` | `setMode` | `string` | — | Switch the machine between auto / manual control modes. | | `cmd.startup` | _(none)_ | `any` | — | Initiate the machine startup sequence. | | `cmd.shutdown` | _(none)_ | `any` | — | Initiate the machine shutdown sequence. | | `cmd.estop` | `emergencystop` | `any` | — | Trigger an emergency stop. | | `execSequence` | _(none)_ | `object` | — | Legacy umbrella that demuxes payload.action to startup / shutdown. | | `set.setpoint` | `execMovement` | `object` | — | Move the machine to a control-% setpoint via execMovement. | | `set.flow-setpoint` | `flowMovement` | `object` | `volumeFlowRate` (default `m3/h`) | Move the machine to a flow setpoint via flowMovement. | | `data.simulate-measurement` | `simulateMeasurement` | `object` | — | Inject a simulated sensor reading (pressure/flow/temperature/power). | | `query.curves` | `showWorkingCurves` | `any` | — | Return the working curves for the machine on the reply port. | | `query.cog` | `CoG` | `any` | — | Return the centre-of-gravity (CoG) point on the reply port. | | `child.register` | `registerChild` | `string` | — | Register a child measurement with this machine. | ## 6. Child registration `measurement` children register through `childRegistrationUtils`; the machine subscribes to the matching `.measured.` event. ```mermaid flowchart LR subgraph kids["accepted children (softwareType)"] m_pu["measurement
type=pressure
position=upstream"]:::ctrl m_pd["measurement
type=pressure
position=downstream"]:::ctrl m_f["measurement
type=flow"]:::ctrl m_pw["measurement
type=power"]:::ctrl m_t["measurement
type=temperature"]:::ctrl end m_pu -->|pressure.measured.upstream| router[pressureRouter.route] m_pd -->|pressure.measured.downstream| router m_f -->|flow.measured.| mh[measurementHandlers] m_pw -->|power.measured.atequipment| mh m_t -->|temperature.measured.| mh router --> upd[updatePosition + drift refresh] mh --> upd classDef ctrl fill:#a9daee,color:#000 ``` | softwareType | filter | wired to | side-effect | |---|---|---|---| | `measurement` | `type=pressure, position=upstream` | `pressureRouter.route('upstream', ...)` | Sets upstream pressure; refresh prediction + drift. | | `measurement` | `type=pressure, position=downstream` | `pressureRouter.route('downstream', ...)` | Sets downstream pressure; refresh prediction + drift. | | `measurement` | `type=flow, position=*` | `measurementHandlers.updateMeasuredFlow` | Stored; drift assessed against predicted. | | `measurement` | `type=power, position=atEquipment` | `measurementHandlers.updateMeasuredPower` | Stored; drift assessed against predicted. | | `measurement` | `type=temperature, position=*` | `measurementHandlers.updateMeasuredTemperature` | Stored; used by power correction if relevant. | Two **virtual children** are auto-registered at startup: `dashboard-sim-upstream` and `dashboard-sim-downstream`. `data.simulate-measurement` payloads land on these. Real pressure children, when registered, are preferred over the virtuals by `pressureSelector`. ## 7. Lifecycle — what one event does ```mermaid sequenceDiagram participant parent as MGC / pumpingStation participant rm as rotatingMachine participant fsm as state FSM participant pred as predictors participant out as Port-0 output parent->>rm: flowmovement (Q) rm->>rm: flowController.handle('parent', 'flowmovement', Q) rm->>fsm: setpoint(Q) → maybe transitionToState('accelerating') Note over fsm: state.emitter 'positionChange' per tick fsm-->>rm: positionChange → updatePosition() rm->>pred: calcFlowPower(x) → cFlow, cPower rm->>rm: calcEfficiency / cog / distance-BEP rm->>rm: drift refresh on every measured tick rm->>out: msg{topic, payload} (delta-compressed) parent->>rm: execsequence ('startup' | 'shutdown') rm->>fsm: transitionToState('starting' | 'stopping') fsm-->>rm: stateChange → _updateState() ``` ## 8. Data model — `getOutput()` Composed in `io/output.js → buildOutput(this)`, then delta-compressed. | Key | Type | Unit | Sample | |---|---|---|---| | `NCog` | number | — | `0` | | `NCogPercent` | number | — | `0` | | `atmPressure.measured.atequipment.wikigen-rotatingmachine-id` | number | — | `101325` | | `cog` | number | — | `0` | | `ctrl` | number | — | `0` | | `effDistFromPeak` | number | — | `0` | | `effRelDistFromPeak` | number | — | `0` | | `flow.predicted.max.wikigen-rotatingmachine-id` | number | m3/s | `0` | | `flow.predicted.min.wikigen-rotatingmachine-id` | number | m3/s | `0` | | `maintenanceTime` | number | — | `0` | | `mode` | string | — | `"auto"` | | `moveTimeleft` | number | — | `0` | | `predictionConfidence` | number | — | `0` | | `predictionFlags` | array | — | `[…]` | | `predictionPressureSource` | null | — | `null` | | `predictionQuality` | string | — | `"invalid"` | | `pressureDriftFlags` | array | — | `[…]` | | `pressureDriftLevel` | number | — | `0` | | `pressureDriftSource` | null | — | `null` | | `runtime` | number | — | `0` | | `state` | string | — | `"idle"` | | `temperature.measured.atequipment.wikigen-rotatingmachine-id` | number | K | `15` | **Concrete sample** (live, from a known-good test run — pump warming up with simulated upstream/downstream pressure): ~~~json { "state": "warmingup", "ctrl": 42.5, "mode": "auto", "runtime": 0.0014, "flow.predicted.downstream.default": 12.4, "flow.predicted.atequipment.default": 12.4, "flow.predicted.max.dashboard-sim-upstream": 22.1, "flow.predicted.min.dashboard-sim-upstream": 0, "power.predicted.atequipment.default": 18.2, "pressure.measured.upstream.dashboard-sim-upstream": 101325, "pressure.measured.downstream.dashboard-sim-downstream": 145000, "temperature.measured.atequipment.dashboard-sim-upstream": 15, "atmPressure.measured.atequipment.dashboard-sim-upstream": 101325, "predictionQuality": "warming", "predictionConfidence": 0.35, "predictionPressureSource": "dashboard-sim", "predictionFlags": ["pressure_init_warming"], "pressureDriftLevel": 0, "pressureDriftSource": null, "pressureDriftFlags": ["nominal"], "cog": 0.62, "NCog": 0.71, "NCogPercent": 62, "effDistFromPeak": 0.04, "effRelDistFromPeak": 0.12, "moveTimeleft": 0, "maintenanceTime": 0 } ~~~ Position labels are normalised to lowercase in MeasurementContainer keys (`atequipment`, `downstream`, `upstream`, `max`, `min`). The trailing `` segment is the registering child's id (or `default` for own predictions / virtuals tagged via `dashboard-sim-*`). ## 9. Configuration — editor form ↔ config keys ```mermaid flowchart TB subgraph editor["Node-RED editor form"] f1[Asset — supplier / category / model / unit] f2[Position vs parent] f3[State times: startup / warmup / shutdown / cooldown] f4[Movement mode + reaction speed] f5[Process output format] f6[Database output format] f7[Logger — level / enabled] end subgraph cfg["Domain config slice"] c1[asset.model / asset.unit / asset.supplier / asset.category] c2[functionality.positionVsParent] c3[time.starting / warmingup / stopping / coolingdown] c4[movement.mode / movement.speed] c5[output.process] c6[output.dbase] c7[general.logging] end f1 --> c1 f2 --> c2 f3 --> c3 f4 --> c4 f5 --> c5 f6 --> c6 f7 --> c7 ``` | Form field | Config key | Default | Range | Where used | |---|---|---|---|---| | Asset model | `asset.model` | `Unknown` | string (must resolve in curve loader) | `_setupCurves` | | Asset flow unit | `asset.unit` | `m3/h` | unit string | unit policy `output.flow` | | Position vs parent | `functionality.positionVsParent` | `atEquipment` | enum (`upstream`, `atEquipment`, `downstream`) | child-register payload + event suffix | | State time — starting | `time.starting` | `10` (s) | ≥ 0 | FSM timing | | State time — warmingup | `time.warmingup` | `5` (s) | ≥ 0 | FSM timing | | State time — stopping | `time.stopping` | `5` (s) | ≥ 0 | FSM timing | | State time — coolingdown | `time.coolingdown` | `10` (s) | ≥ 0 | FSM timing | | Movement mode | `movement.mode` | `staticspeed` | enum (`staticspeed`, `dynspeed`) | position trajectory | | Reaction speed | `movement.speed` | `1` | ≤ `maxSpeed` | trajectory ramp rate (%/s) | | Process output format | `output.process` | `process` | enum (`process`, `json`, `csv`) | Port 0 formatter | | Database output format | `output.dbase` | `influxdb` | enum (`influxdb`, `json`, `csv`) | Port 1 formatter | ## 10. State chart The FSM is the canonical state set declared in `generalFunctions/src/state/stateConfig.json`. `emergencystop` is reachable from *every* state. Allowed transitions per `stateConfig.allowedTransitions`. ```mermaid stateDiagram-v2 [*] --> idle idle --> starting: execsequence(startup) idle --> off: off idle --> maintenance: maintenance starting --> warmingup: timer warmingup --> operational: timer operational --> accelerating: flowmovement / setpoint up operational --> decelerating: flowmovement / setpoint down accelerating --> operational: target reached decelerating --> operational: target reached operational --> stopping: execsequence(shutdown) stopping --> coolingdown: timer stopping --> idle: timer coolingdown --> idle: timer coolingdown --> off: off off --> idle: execsequence(startup) off --> maintenance: maintenance maintenance --> idle: maintenance done maintenance --> off: off note right of operational any state -> emergencystop via cmd.estop end note ``` `accelerating` / `decelerating` are abortable on new demand via `abortMovement(reason)`; the controller does **not** auto-transition back to `operational` after an abort (see `state.js` comment "Abort path"). `warmingup` and `coolingdown` are **protected** — abort signals are dropped for safety. `activeStates = { operational, starting, warmingup, accelerating, decelerating }` is the set MGC treats as "machine alive". ## 11. Examples | Tier | File | What it shows | Status | |---|---|---|---| | Basic | `examples/01 - Basic Manual Control.json` | Inject + dashboard, simulated pressure, manual startup/shutdown | ✅ validated | | Integration | `examples/02 - Integration with Machine Group.json` | rotatingMachine wired under MGC | ⏳ pending validation | | Dashboard | `examples/03 - Dashboard Visualization.json` | FlowFuse charts: flow / power / pressure trends | ✅ in repo | | Legacy | `examples/basic.flow.json` / `integration.flow.json` / `edge.flow.json` | Pre-refactor flows | ⚠️ kept until new Tier 2 is validated | Screenshots will land under `wiki/_partial-screenshots/rotatingMachine/` once captured from the live demo. ## 12. Debug recipes | Symptom | First thing to check | Where to look | |---|---|---| | `state` stuck on `idle`, no startup | Source not in `mode.allowedSources[currentMode]`. Check `flowController` warn log. | `_setupState` + `isValidSourceForMode`. | | `flow.predicted.*` is 0 or `NaN` | Pressure not initialised — `predictionHealth.flags` will say `pressure_init_warming`. Inject pressure via `data.simulate-measurement` or wire real measurement children. | `getMeasuredPressure` + `pressureSelector`. | | `predictionHealth.quality='invalid'` | Curve normalisation failed at startup — null predictors installed. Check container log for `Curve normalization failed for model …`. | `_setupCurves`. | | Drift `level=3` after startup | Less than 10 paired samples (`minSamplesForLongTerm`) — wait a few ticks before judging. | `driftProfiles.minSamplesForLongTerm`. | | `cmd.estop` doesn't recover | After `emergencystop`, only `idle` / `off` / `maintenance` are allowed. Send `cmd.shutdown` then `cmd.startup`, or reset via maintenance. | `stateConfig.allowedTransitions.emergencystop`. | | Position bounces around target | Movement mode `dynspeed` ease-in/out may overshoot at high speed; try `staticspeed`. | `movement.mode`. | > Never ship `enableLog: 'debug'` in a demo — fills the container log within seconds and obscures real errors. ## 13. When you would NOT use this node - Use rotatingMachine for a **single** pump / compressor / blower. For groups of 2+ with load sharing, wire `machineGroupControl` as the parent. - Don't use rotatingMachine to model a **passive non-return valve** — use `valve` (no curve, no FSM-driven motor). - Don't use rotatingMachine without a **curve model** — flow / power predictions degrade to zero and drift is meaningless. ## 14. Known limitations / current issues | # | Issue | Tracked in | |---|---|---| | 1 | Drift confidence drops to 0 when pressure source is missing > 30 s — health flips to `invalid` silently. | `pressure/pressureInitialization.js`. | | 2 | Multi-parent registration accepted by `childRegistrationUtils` but ordering of teardown is not test-covered. | Open question — `OPEN_QUESTIONS.md`. | | 3 | `data.simulate-measurement` does not unset previous values on missing keys — stale sim data can persist after toggling off. | `measurementHandlers.updateSimulatedMeasurement`. | | 4 | `execSequence` legacy umbrella topic kept alive in registry; planned removal in Phase 7. | `commands/index.js` `_legacy: true`. |