Table of Contents
- rotatingMachine
- 1. What this node is
- 2. Position in the platform
- 3. Capability matrix
- 4. Code map
- 5. Topic contract
- 6. Child registration
- 7. Lifecycle — what one event does
- 8. Data model — getOutput()
- 9. Configuration — editor form ↔ config keys
- 10. State chart
- 11. Examples
- 12. Debug recipes
- 13. When you would NOT use this node
- 14. Known limitations / current issues
rotatingMachine
Reflects code as of
afc304b· regenerated2026-05-11vianpm run wiki:allIf 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
flowchart LR
parent[machineGroupControl /<br/>pumpingStation]:::unit -->|flowmovement<br/>execsequence| rm[rotatingMachine<br/>Equipment]:::equip
m_up[measurement<br/>pressure upstream]:::ctrl -.data.-> rm
m_dn[measurement<br/>pressure downstream]:::ctrl -.data.-> rm
sim[dashboard-sim<br/>virtual pressure children]:::ctrl -.data.-> rm
rm -->|child.register| parent
rm -.->|flow.predicted.*<br/>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
flowchart TB
subgraph nodeRED["nodeClass.js — adapter (BaseNodeAdapter)"]
nc["buildDomainConfig()<br/>static DomainClass, commands"]
end
subgraph domain["specificClass.js — orchestrator (BaseDomain)"]
sc["Machine.configure()<br/>_setupCurves / _setupState /<br/>_setupDrift / _setupPressure /<br/>_setupChildren"]
end
subgraph concerns["src/ concern modules"]
curves["curves/<br/>loadModelCurve + normalize"]
prediction["prediction/<br/>buildPredictors + math"]
drift["drift/<br/>DriftAssessor + healthRefresh"]
pressure["pressure/<br/>init + router + selector + virtual"]
state["state/<br/>FSM bindings + sequenceController"]
measurement["measurement/<br/>handlers + childRegistrar"]
flow["flow/<br/>flowController (handleInput)"]
display["display/<br/>workingCurves + CoG"]
io["io/<br/>output + status"]
commands["commands/<br/>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-runnpm 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 <asset.type>.measured.<positionVsParent> event.
flowchart LR
subgraph kids["accepted children (softwareType)"]
m_pu["measurement<br/>type=pressure<br/>position=upstream"]:::ctrl
m_pd["measurement<br/>type=pressure<br/>position=downstream"]:::ctrl
m_f["measurement<br/>type=flow"]:::ctrl
m_pw["measurement<br/>type=power"]:::ctrl
m_t["measurement<br/>type=temperature"]:::ctrl
end
m_pu -->|pressure.measured.upstream| router[pressureRouter.route]
m_pd -->|pressure.measured.downstream| router
m_f -->|flow.measured.<pos>| mh[measurementHandlers]
m_pw -->|power.measured.atequipment| mh
m_t -->|temperature.measured.<pos>| 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
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):
{
"state": "warmingup",
"flow.predicted.downstream.default": 0.00345,
"flow.predicted.atequipment.default": 0.00345,
"power.predicted.atequipment.default": 1820,
"pressure.measured.upstream.dashboard-sim-upstream": 101325,
"pressure.measured.downstream.dashboard-sim-downstream": 145000,
"temperature.measured.atequipment.default": 15,
"atmPressure.measured.atequipment.default": 101325,
"predictionHealth": {
"quality": "warming",
"confidence": 0.35,
"pressureSource": "dashboard-sim",
"flags": ["pressure_init_warming"]
},
"cog": 0.62, "NCog": 0.71,
"absDistFromPeak": 0.04, "relDistFromPeak": 0.12
}
Position labels are normalised to lowercase in MeasurementContainer keys (atequipment, downstream, upstream, max, min). The trailing <childId> segment is the registering child's id (or default for own predictions / virtuals tagged via dashboard-sim-*).
9. Configuration — editor form ↔ config keys
flowchart TB
subgraph editor["Node-RED editor form"]
f1[Asset model dropdown]
f2[Mode current]
f3[Position vs parent]
f4[State times: starting, warmingup, ...]
f5[Movement mode + speed]
f6[Position min / max / initial]
f7[Allowed sources / actions per mode]
f8[Output unit (flow, pressure, power)]
end
subgraph cfg["Domain config slice"]
c1[asset.model]
c2[mode.current]
c3[functionality.positionVsParent]
c4[time.starting / warmingup / stopping / coolingdown]
c5[movement.mode / speed / maxSpeed / interval]
c6[position.min / max / initial]
c7[mode.allowedSources / allowedActions]
c8[general.unit / asset.unit]
end
f1 --> c1
f2 --> c2
f3 --> c3
f4 --> c4
f5 --> c5
f6 --> c6
f7 --> c7
f8 --> c8
| Form field | Config key | Default | Range | Where used |
|---|---|---|---|---|
| Asset model | asset.model |
Unknown |
string (must resolve in curve loader) | _setupCurves |
| Mode | mode.current |
auto |
enum (auto, manual) |
flowController.handle source check |
| Position vs parent | functionality.positionVsParent |
atEquipment |
enum | 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 |
dynspeed |
enum (staticspeed, dynspeed) |
position trajectory |
| Movement speed | movement.speed |
1 |
≤ maxSpeed |
trajectory rate |
| Position min/max | position.min / position.max |
0 / 100 |
numeric | setpoint clamp |
| Output unit (flow) | general.unit |
l/s |
unit string | unit policy output.flow |
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.
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
machineGroupControlas 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. |