Files
rotatingMachine/wiki/Home.md
znetsixe 9e8463b41d P9.3: wiki/Home.md following 14-section visual-first template + wiki:* scripts
Auto-generated topic-contract + data-model sections via shared wikiGen
script. Hand-written Mermaid diagrams for position-in-platform, code
map, child registration, lifecycle, configuration, state chart (where
applicable).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:17:36 +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 Effect
set.mode setMode string Replaces the named state value with the supplied payload.
cmd.startup (none) any Triggers an action / sequence — not idempotent.
cmd.shutdown (none) any Triggers an action / sequence — not idempotent.
cmd.estop emergencystop any Triggers an action / sequence — not idempotent.
execSequence (none) object (see handler)
set.setpoint execMovement object Replaces the named state value with the supplied payload.
set.flow-setpoint flowMovement object Replaces the named state value with the supplied payload.
data.simulate-measurement simulateMeasurement object Pushes a value into the node's measurement stream.
query.curves showWorkingCurves any Read-only query; node replies on the same msg.
query.cog CoG any Read-only query; node replies on the same msg.
child.register registerChild string Parent/child plumbing — registers or unregisters a child node.

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.