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>
9.0 KiB
rotatingMachine
A rotatingMachine models a single pump, compressor, or blower. It loads a supplier characteristic curve, takes upstream + downstream pressure measurements (real or simulated), 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 pumpingStation for a one-pump station.
At a glance
| Thing | Value |
|---|---|
| What it represents | One rotating asset on a curve — pump, blower, compressor |
| S88 level | Equipment Module |
| Use it when | You have a curve-modelled asset whose flow / power varies with header differential and you want predictions + drift |
| Don't use it for | Passive non-return valves (valve), curveless assets (will silently emit zeros), groups (parent under machineGroupControl) |
| Children it accepts | measurement (pressure / flow / power / temperature) |
| Parents it talks to | machineGroupControl, pumpingStation, or any node that issues flowmovement / execsequence |
How it fits
flowchart LR
parent[machineGroupControl /<br/>pumpingStation]:::unit -->|flowmovement<br/>execsequence| rm[rotatingMachine<br/>Equipment]:::equip
m_up[measurement<br/>pressure upstream]:::ctrl -.measured.-> rm
m_dn[measurement<br/>pressure downstream]:::ctrl -.measured.-> rm
sim[dashboard-sim-upstream /<br/>dashboard-sim-downstream<br/>(auto-registered virtual children)]:::ctrl -.measured.-> 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 are anchored in .claude/rules/node-red-flow-layout.md.
Try it — 3-minute demo
Import the basic example flow, deploy, and drive a single pump through the full state machine.
curl -X POST -H 'Content-Type: application/json' \
--data @nodes/rotatingMachine/examples/01\ -\ Basic\ Manual\ Control.json \
http://localhost:1880/flow
What to click after deploy (the inject buttons map one-to-one to topics in Reference — Contracts):
data.simulate-measurement(upstream + downstream) — injects ~0 mbar suction and ~1100 mbar discharge so the predictor has something to work with.set.mode = virtualControl— lets the GUI source drive the pump (parent path is for grouped use).cmd.startup— FSM runsidle → starting → warmingup → operational.runtimestarts accumulating.set.setpoint = 60(control %) — pump ramps from0to60at the configuredReaction Speed; state goesoperational → accelerating → operational.set.flow-setpoint = {value: 80, unit: "m3/h"}— same path, but the setpoint is a flow value; the node converts viapredictCtrlto a control %.cmd.shutdown—operational → decelerating → stopping → coolingdown → idle.
Important
GIF needed. Demo recording of steps 1–6 with the live status panel. Save as
wiki/_partial-gifs/rotatingMachine/01-basic-demo.gif, target ≤ 1 MB aftergifsicle -O3 --lossy=80.
The seven things you'll send
| Topic | Aliases | Payload | What it does |
|---|---|---|---|
set.mode |
setMode |
"auto" | "virtualControl" | "fysicalControl" |
Switch between parent-controlled, GUI-controlled, and physical-source-only. Each mode has its own allow-list for actions and sources. |
cmd.startup |
— | any | Run the configured startup sequence (default [starting, warmingup, operational]). |
cmd.shutdown |
— | any | Run the configured shutdown sequence (default [stopping, coolingdown, idle]). operational triggers a ramp-to-zero first. |
cmd.estop |
emergencystop |
any | Hard cut: runs the emergencystop sequence (default [emergencystop, off]). Reachable from every state. |
set.setpoint |
execMovement |
{setpoint: number} (control %) |
Move to a control-% setpoint. |
set.flow-setpoint |
flowMovement |
{setpoint: number} (flow, unit per units) |
Move to a flow setpoint. Converted to canonical m³/s, then to control % via predictCtrl. |
data.simulate-measurement |
simulateMeasurement |
{asset: {type, unit}, value, position, childId?} |
Inject a virtual sensor reading (pressure / flow / power / temperature). |
Plus two query topics for dashboards:
| Topic | Aliases | Returns on the reply port |
|---|---|---|
query.curves |
showWorkingCurves |
The working curves (flow / power / efficiency) at the current operating point. |
query.cog |
CoG |
The centre-of-gravity (CoG) of the η curve. |
What you'll see come out
Sample Port 0 message (delta-compressed, while operational at ~60 % control):
{
"topic": "rotatingMachine#pump_a",
"payload": {
"state": "operational",
"ctrl": 60.0,
"mode": "auto",
"runtime": 0.024,
"flow.predicted.downstream.default": 12.4,
"flow.predicted.atequipment.default": 12.4,
"power.predicted.atequipment.default": 18.2,
"pressure.measured.upstream.dashboard-sim-upstream": 0,
"pressure.measured.downstream.dashboard-sim-downstream": 1100,
"predictionQuality": "good",
"predictionConfidence": 0.92,
"predictionPressureSource": "dashboard-sim",
"predictionFlags": [],
"cog": 0.62, "NCog": 0.71, "NCogPercent": 62,
"effDistFromPeak": 0.04, "effRelDistFromPeak": 0.12
}
}
Key shape: <type>.<variant>.<position>.<childId> — the inverse of MGC's key shape, because rotatingMachine emits per-measurement snapshots. The trailing <childId> is the registering child's id (dashboard-sim-upstream, dashboard-sim-downstream, or default for own predictions). Position labels are normalised to lowercase in keys.
| Field | Meaning |
|---|---|
state |
Current FSM state. See Architecture — FSM. |
ctrl |
Control-axis position (0..100). |
mode |
One of auto / virtualControl / fysicalControl. |
runtime |
Accumulated hours in active states (operational and movement variants). |
flow.predicted.{downstream,atequipment}.default |
Predicted flow at the current operating point (canonical m³/s; renders to m3/h). |
power.predicted.atequipment.default |
Predicted shaft power (canonical W; renders to kW). |
predictionQuality |
good / warming / degraded / invalid — derived by predictionHealth from drift + pressure availability. |
predictionPressureSource |
dashboard-sim (virtual children active) or a real-child id (real children preferred). |
predictionFlags |
Reason codes when health < good (e.g. pressure_init_warming, flow_high_drift). |
cog / NCog / NCogPercent |
Centre-of-gravity metric on the η curve. NCog is normalised 0..1. |
effDistFromPeak / effRelDistFromPeak |
Distance from the η peak (absolute and 0..1 relative). |
The new bit — sequence-abort token
When a parent MGC sends a new demand, it calls abortMovement to interrupt any in-flight accelerating / decelerating movement. Before 2026-05-15 that abort only stopped the moveTo — an in-flight executeSequence('shutdown') for-loop would keep transitioning the FSM through stopping → coolingdown → idle, fighting the new dispatch's residue-handler.
The pump now carries a monotonic sequenceAbortToken on its state object. External aborts (the kind MGC fires) advance it; sequence-internal aborts (e.g. shutdown's own pre-empt of its ramp-down step) do not. executeSequence captures the token at entry and bails out before its next transition if the counter has advanced.
Net effect: a mid-decel re-engage takes the pump cleanly back to operational, without the orphaned shutdown completing in the background. warmingup and coolingdown remain protected at the stateManager layer — safety guarantees are unchanged.
See Architecture — FSM for the full mechanism.
Need more?
| Page | What you'll find |
|---|---|
| Reference — Contracts | Full topic contract, config schema, child registration filters |
| Reference — Architecture | Code map, FSM, prediction pipeline, drift, lifecycle |
| Reference — Examples | Shipped example flows + debug recipes |
| Reference — Limitations | When not to use, known limitations, open questions |