Files
rotatingMachine/wiki/Home.md
znetsixe 1a9f533b1e P11.6 wiki regen + Phase 10 private-test rewrites where applicable
For all 11 nodes with auto-gen markers: wiki/Home.md sections 5 (topic
contract) and 9 (data model) regenerated via npm run wiki:all. New
Unit column shows '<measure> (default <unit>)' for declared topics,
'—' otherwise. Effect column now uses descriptor.description (P11.2
field) overriding the generic per-prefix fallback.

For rotatingMachine + reactor: Phase 10 test rewrites — 3 + 8 files
moved off private nodeClass internals (_attachInputHandler, _commands,
_pendingExtras, _registerChild, _tick, etc.) to the public
BaseNodeAdapter surface (node.handlers.input, node.source.*).
+6 / +7 net new tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 19:44:11 +02:00

18 KiB

rotatingMachine

Reflects code as of afc304b · 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

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-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 <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 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.