diff --git a/src/state/sequenceController.js b/src/state/sequenceController.js
index 5126612..0629afa 100644
--- a/src/state/sequenceController.js
+++ b/src/state/sequenceController.js
@@ -63,6 +63,15 @@ async function executeSequence(host, rawName) {
host.logger.warn(`Sequence '${name}' not defined.`);
return;
}
+ // Snapshot the sequence-abort token at entry, BEFORE any awaits. If an
+ // external abort advances the counter while we're inside this call
+ // (setpoint ramp-down, waitForOperational, or the state transition
+ // loop), every check below sees the mismatch and breaks out so the
+ // new dispatch can claim the FSM. Capturing later would conflate the
+ // abort that fired during setpoint(0) with the initial entry state.
+ const startToken = host.state.sequenceAbortToken ?? 0;
+ const aborted = () => (host.state.sequenceAbortToken ?? 0) !== startToken;
+
const interruptible = new Set(['shutdown', 'emergencystop']);
if (interruptible.has(name)) host.state.delayedMove = null;
const current = host.state.getCurrentState();
@@ -74,9 +83,18 @@ async function executeSequence(host, rawName) {
if (host.state.getCurrentState() === 'operational' && name === 'shutdown') {
host.logger.info(`Machine will ramp down to position 0 before performing ${name} sequence`);
await setpoint(host, 0);
+ if (aborted()) {
+ host.logger.warn(`Sequence '${name}' interrupted during ramp-down by external abort; not entering shutdown loop.`);
+ host.updatePosition();
+ return;
+ }
}
host.logger.info(` --------- Executing sequence: ${name} -------------`);
for (const s of sequence) {
+ if (aborted()) {
+ host.logger.warn(`Sequence '${name}' interrupted at step '${s}' by external abort; stopping further transitions.`);
+ break;
+ }
try { await host.state.transitionToState(s); }
catch (e) { host.logger.error(`Error during sequence '${name}': ${e}`); break; }
}
diff --git a/wiki/Home.md b/wiki/Home.md
index f455af8..aeb7a82 100644
--- a/wiki/Home.md
+++ b/wiki/Home.md
@@ -1,20 +1,32 @@
# 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
+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.
-**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
+## 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
```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
+ m_up[measurement pressure upstream]:::ctrl -.measured.-> rm
+ m_dn[measurement pressure downstream]:::ctrl -.measured.-> rm
+ sim[dashboard-sim-upstream / dashboard-sim-downstream (auto-registered virtual children)]:::ctrl -.measured.-> rm
rm -->|child.register| parent
rm -.->|flow.predicted.* power.predicted.atequipment| parent
classDef unit fill:#50a8d9,color:#000
@@ -22,331 +34,119 @@ flowchart LR
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`.
+S88 colours are anchored in `.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. |
+## Try it — 3-minute demo
-## 4. Code map
+Import the basic example flow, deploy, and drive a single pump through the full state machine.
-```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
+```bash
+curl -X POST -H 'Content-Type: application/json' \
+ --data @nodes/rotatingMachine/examples/01\ -\ Basic\ Manual\ Control.json \
+ http://localhost:1880/flow
```
-| 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. |
+What to click after deploy (the inject buttons map one-to-one to topics in [Reference — Contracts](Reference-Contracts#topic-contract)):
-## 5. Topic contract
+1. `data.simulate-measurement` (upstream + downstream) — injects ~0 mbar suction and ~1100 mbar discharge so the predictor has something to work with.
+2. `set.mode = virtualControl` — lets the GUI source drive the pump (parent path is for grouped use).
+3. `cmd.startup` — FSM runs `idle → starting → warmingup → operational`. `runtime` starts accumulating.
+4. `set.setpoint = 60` (control %) — pump ramps from `0` to `60` at the configured `Reaction Speed`; state goes `operational → accelerating → operational`.
+5. `set.flow-setpoint = {value: 80, unit: "m3/h"}` — same path, but the setpoint is a flow value; the node converts via `predictCtrl` to a control %.
+6. `cmd.shutdown` — `operational → decelerating → stopping → coolingdown → idle`.
-> **Auto-generated** from `src/commands/index.js`. Do NOT hand-edit between the markers. Re-run `npm run wiki:contract`.
+> [!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 after `gifsicle -O3 --lossy=80`.
-
+---
-| 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. |
+## 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). |
-## 6. Child registration
+Plus two query topics for dashboards:
-`measurement` children register through `childRegistrationUtils`; the machine subscribes to the matching `.measured.` event.
+| 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. |
-```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. |
+## What you'll see come out
-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`.
+Sample Port 0 message (delta-compressed, while operational at ~60 % control):
-## 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
+```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
+ "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
+ }
}
-~~~
-
-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 |
+Key shape: **`...`** — the inverse of MGC's key shape, because rotatingMachine emits per-measurement snapshots. The trailing `` 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.
-## 10. State chart
+| Field | Meaning |
+|:---|:---|
+| `state` | Current FSM state. See [Architecture — FSM](Reference-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 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
+## The new bit — sequence-abort token
- note right of operational
- any state -> emergencystop
- via cmd.estop
- end note
-```
+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.
-`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".
+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.
-## 11. Examples
+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.
-| 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 |
+See [Architecture — FSM](Reference-Architecture#fsm) for the full mechanism.
-Screenshots will land under `wiki/_partial-screenshots/rotatingMachine/` once captured from the live demo.
+---
-## 12. Debug recipes
+## Need more?
-| 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`. |
+| Page | What you'll find |
+|:---|:---|
+| [Reference — Contracts](Reference-Contracts) | Full topic contract, config schema, child registration filters |
+| [Reference — Architecture](Reference-Architecture) | Code map, FSM, prediction pipeline, drift, lifecycle |
+| [Reference — Examples](Reference-Examples) | Shipped example flows + debug recipes |
+| [Reference — Limitations](Reference-Limitations) | When not to use, known limitations, open questions |
-> 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`. |
+[EVOLV master wiki](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Home) · [Topology Patterns](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topology-Patterns) · [Topic Conventions](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions)
diff --git a/wiki/Reference-Architecture.md b/wiki/Reference-Architecture.md
new file mode 100644
index 0000000..7e761c8
--- /dev/null
+++ b/wiki/Reference-Architecture.md
@@ -0,0 +1,340 @@
+# Reference — Architecture
+
+
+
+> [!NOTE]
+> Code structure for `rotatingMachine`: the three-tier sandwich, the `src/` layout, the FSM (with the new sequence-abort token), the prediction + drift pipeline, the lifecycle, and the output-port pipeline. For an intuitive overview, return to [Home](Home).
+
+---
+
+## Three-tier code layout
+
+```
+nodes/rotatingMachine/
+|
++-- rotatingMachine.js entry: RED.nodes.registerType('rotatingMachine', NodeClass)
+|
++-- src/
+| nodeClass.js extends BaseNodeAdapter (Node-RED bridge)
+| specificClass.js extends BaseDomain (orchestration only)
+| |
+| +-- commands/
+| | index.js topic descriptors
+| | handlers.js pure handler functions
+| |
+| +-- curves/
+| | curveLoader.js load supplier curve by model id
+| | curveNormalizer.js unit + shape normalisation
+| | reverseCurve.js invert flow → ctrl for predictCtrl
+| |
+| +-- prediction/
+| | predictors.js buildPredictors(curve) → predictFlow / Power / Ctrl
+| | groupPredictors.js buildGroupPredictors() for MGC integration
+| | predictionMath.js calcFlow / calcPower / calcCtrl / inputFlowCalcPower
+| | efficiencyMath.js calcCog / calcEfficiency / calcDistanceBEP
+| | operatingPoint.js legacy hook kept for migrations
+| |
+| +-- drift/
+| | driftAssessor.js per-metric drift pipeline (EWMA + alignment)
+| | healthRefresh.js updates predictionHealth + pressureDrift
+| | predictionHealth.js derives quality / confidence / flags
+| |
+| +-- pressure/
+| | pressureInitialization.js pressure-source readiness tracker
+| | pressureRouter.js routes upstream / downstream measurements
+| | pressureSelector.js pushes fDimension onto predictors
+| | virtualChildren.js auto-registered dashboard-sim children
+| |
+| +-- state/
+| | stateBindings.js wires state.emitter to host callbacks
+| | sequenceController.js setpoint / executeSequence / waitForOperational
+| |
+| +-- measurement/
+| | measurementHandlers.js per-type handlers (flow / power / temperature)
+| | childRegistrar.js filter-aware listener attach / detach
+| |
+| +-- flow/
+| | flowController.js action dispatch (handleInput)
+| |
+| +-- display/
+| | workingCurves.js query.curves / query.cog reply shape
+| |
+| +-- io/
+| output.js getOutput() shape + status badge
+```
+
+### Tier responsibilities
+
+| Tier | File | What it owns | Touches `RED.*` |
+|:---|:---|:---|:---:|
+| entry | `rotatingMachine.js` | Type registration | Yes |
+| nodeClass | `src/nodeClass.js` | Input routing, output ports, status-badge polling (`statusInterval=1000`). Stashes `stateConfig` and `errorMetricsConfig` on the class for the constructor. No tick loop — event-driven. | Yes |
+| specificClass | `src/specificClass.js` | Wire concern modules in `configure()`; expose the same public surface MGC + pumpingStation already call (`handleInput`, `abortMovement`, `setGroupOperatingPoint`, `registerChild`, …); delegate everything else. | No |
+
+`specificClass` is stitching. All real work lives in the concern modules: pure math in `prediction/`, `drift/`; live-state-touching in `pressure/`, `state/`, `measurement/`, `flow/`.
+
+---
+
+## FSM
+
+The state machine is declared in `generalFunctions/src/state/stateConfig.json`. Allowed transitions (relevant subset):
+
+```mermaid
+stateDiagram-v2
+ [*] --> idle
+ idle --> starting: startup
+ idle --> off
+ idle --> maintenance
+ starting --> warmingup: timer (time.starting)
+ warmingup --> operational: timer (time.warmingup) [protected]
+ operational --> accelerating: setpoint up
+ operational --> decelerating: setpoint down
+ operational --> stopping: shutdown
+ accelerating --> operational: target reached
+ decelerating --> operational: target reached
+ stopping --> coolingdown: timer (time.stopping)
+ coolingdown --> idle: timer (time.coolingdown) [protected]
+ coolingdown --> off
+ off --> idle: boot (first step)
+ off --> maintenance
+ maintenance --> off: exitmaintenance (step 1)
+ maintenance --> idle
+
+ note right of operational
+ any state -> emergencystop via cmd.estop
+ from emergencystop: idle / off / maintenance
+ end note
+```
+
+Allowed transitions are declared in `generalFunctions/src/state/stateConfig.json` `allowedTransitions`. The diagram omits the `emergencystop` arrows for readability — every state has one. Self-edges (`starting → starting`, `maintenance → maintenance`) exist in the config for re-entrancy but aren't load-bearing.
+
+### Protected states
+
+`warmingup` and `coolingdown` are **protected** in `state.js` `transitionToState`. When the FROM-state is one of these, the abort signal passed to `stateManager.transitionTo` is nulled out:
+
+```js
+const protectedStates = ['warmingup', 'coolingdown'];
+const isProtectedTransition = protectedStates.includes(fromState);
+if (isProtectedTransition) {
+ signal = null;
+ this.logger.warn(`Transition from ${fromState} to ${targetState} is protected and cannot be aborted.`);
+}
+```
+
+So `abortCurrentMovement` cannot interrupt a warmup or cooldown. This is a deliberate safety guarantee — aborting a motor warmup risks burn-up.
+
+### Routine vs sequence-internal aborts
+
+`state.abortCurrentMovement(reason, options)` accepts:
+
+| Option | Default | Used by | Effect |
+|:---|:---|:---|:---|
+| `returnToOperational: false` | yes (default) | MGC's `abortActiveMovements` — new-demand aborts | Aborts the moveTo. Does NOT auto-transition to operational (avoids a bounce loop on per-tick aborts). **Advances `sequenceAbortToken`** so any in-flight `executeSequence` bails out. |
+| `returnToOperational: true` | — | `executeSequence` itself when a fresher shutdown / e-stop pre-empts its own setpoint-to-zero step | Aborts the moveTo and auto-transitions back to operational so the sequence can proceed. Does NOT advance `sequenceAbortToken`. |
+
+### Sequence-abort token — what it does
+
+`state.sequenceAbortToken` is a monotonic counter, advanced on every external (non-internal) abort. `sequenceController.executeSequence` captures the value at entry:
+
+```js
+const startToken = host.state.sequenceAbortToken ?? 0;
+const aborted = () => (host.state.sequenceAbortToken ?? 0) !== startToken;
+```
+
+and checks before:
+
+1. Entering the for-loop (after the optional `setpoint(host, 0)` ramp-down step).
+2. Every iteration of the state-transition for-loop.
+
+A mismatch breaks the loop early with `Sequence '' interrupted ... by external abort`. The pump's `updatePosition` runs anyway so output state stays consistent.
+
+Why this matters: without the token, a shutdown's for-loop continues to run after `abortMovement` rejects its `setpoint(host, 0)`. The pump can transition `operational → stopping → coolingdown → idle` even when a new dispatch has already taken the FSM back to operational via the residue handler. The token snapshot ensures only **one** of those two paths wins per dispatch.
+
+### Residue-state handling in `moveTo`
+
+`state.moveTo` recognises `accelerating` and `decelerating` as **post-abort residue states**. If a setpoint arrives in either, it transitions back to `operational` first, then proceeds with the new move:
+
+```js
+const movementResidueStates = ['accelerating', 'decelerating'];
+if (movementResidueStates.includes(this.stateManager.getCurrentState())) {
+ await this.transitionToState("operational");
+ // Fall through — state is now operational, proceed with new move.
+}
+```
+
+This is what makes mid-flight retargets work without parking the new setpoint in `delayedMove`.
+
+### `delayedMove` — deferred setpoint
+
+When a setpoint arrives while the FSM is in a genuinely non-operational, non-residue state (`starting`, `warmingup`, `stopping`, `coolingdown`, `idle`, `off`, `emergencystop`, `maintenance`) AND mode is `auto`, the value is stashed in `state.delayedMove`. The next transition INTO `operational` picks it up and fires `moveTo(delayedMove)`. So a flow setpoint sent during startup is queued, not lost.
+
+### State-entry timestamp + remaining transition
+
+`stateManager.stateEnteredAt` is wall-clock-stamped on every state assignment (constructor + both transition branches). `stateManager.getRemainingTransitionS()` returns `max(0, transitionTimes[currentState] − elapsed)`. The MGC movement planner calls this through `machineProfile.buildProfile` to compute exact rendezvous time for pumps currently in `warmingup` / `starting`.
+
+---
+
+## Prediction + drift pipeline
+
+```mermaid
+flowchart TB
+ sim[data.simulate-measurement]:::input --> pi[pressureInitialization]
+ real[measurement child pressure.measured.up/down]:::input --> pi
+ pi --> ps[pressureSelector prefers real over virtual]
+ ps --> fd[fDimension push: predictFlow / predictPower / predictCtrl]
+ fd --> upd[updatePosition()]
+ upd --> calc[calcFlowPower(ctrl)]
+ calc --> meas[MeasurementContainer flow.predicted.* power.predicted.atequipment]
+ measFlow[flow.measured.*]:::input --> drift[DriftAssessor EWMA + alignment]
+ measPower[power.measured.atequipment]:::input --> drift
+ meas --> drift
+ drift --> health[predictionHealth.refresh quality / confidence / flags]
+ health --> out[Port 0]
+ upd --> out
+ classDef input fill:#a9daee,color:#000
+```
+
+### Curve loading
+
+At `configure()` startup:
+
+1. `assetResolver.resolveAssetMetadata('rotatingmachine', model)` resolves supplier / type / allowed units from `generalFunctions/datasets/assetData/`.
+2. `asset.unit` is validated (must be a flow unit) and soft-warned if not in the registry's recommended list.
+3. `loadModelCurve(model)` reads the raw supplier curve.
+4. `normalizeMachineCurve(rawCurve, unitPolicy, logger)` unit-converts and shape-normalises.
+5. `buildPredictors(curve)` returns `{predictFlow, predictPower, predictCtrl}` where `predictCtrl` is the reverse curve (flow → control %).
+
+Any failure installs **null predictors** (the asset still loads but emits zeros). The status badge falls through to a `predictionQuality: 'invalid'` state on Port 0.
+
+### Drift
+
+`DriftAssessor` wraps `generalFunctions/nrmse` into per-metric drift profiles. Defaults (`flow` and `power`):
+
+| Field | Value | Notes |
+|:---|:---|:---|
+| `windowSize` | `30` | Sample count for long-term NRMSE |
+| `minSamplesForLongTerm` | `10` | Below this, long-term level stays at 3 (=invalid) |
+| `ewmaAlpha` | `0.15` | Immediate-level smoothing |
+| `alignmentToleranceMs` | `2500` | Predicted ↔ measured timestamps must align within this |
+| `strictValidation` | `true` | Reject samples on alignment failure |
+
+Drift feeds `predictionHealth.refresh` — immediate-level and long-term-level reduce `predictionConfidence` and append `flow_*_drift` / `power_*_drift` flags. Pressure drift is computed separately (real vs virtual divergence).
+
+### Virtual pressure children
+
+Two `measurement`-typed children are auto-registered at startup:
+
+| ID | Position |
+|:---|:---|
+| `dashboard-sim-upstream` | `upstream` |
+| `dashboard-sim-downstream` | `downstream` |
+
+`data.simulate-measurement` payloads land on these. `pressureSelector` prefers any **real** pressure child over the virtuals once one registers; the virtuals stay live so the dashboard can keep injecting test values.
+
+---
+
+## Lifecycle — what one event does
+
+```mermaid
+sequenceDiagram
+ autonumber
+ participant parent as MGC / pumpingStation / GUI
+ participant rm as rotatingMachine
+ participant fc as flowController
+ participant fsm as state (FSM)
+ participant pred as predictors
+ participant out as Port 0 / 1
+
+ parent->>rm: flowmovement (Q, unit)
+ rm->>fc: flowController.handle('parent', 'flowmovement', Q)
+ fc->>fc: mode/source allow-list check
+ fc->>fc: convert Q (output unit → canonical m³/s)
+ fc->>fc: pos = host.calcCtrl(Q)
+ fc->>fsm: setpoint(pos) → state.moveTo(pos)
+ Note over fsm: residue handler may re-enter operational first
+ fsm-->>rm: positionChange events per move tick
+ rm->>pred: calcFlowPower(pos) → cFlow, cPower
+ rm->>rm: calcEfficiency / cog / distance-BEP
+ rm->>out: notifyOutputChanged (Port 0/1 delta)
+ parent->>rm: execsequence ('startup' | 'shutdown')
+ rm->>fsm: executeSequence → state transitions
+ fsm-->>rm: stateChange events → _updateState
+```
+
+### Mode + source allow-lists
+
+Each input is gated twice in `flowController.handle`:
+
+1. `host.isValidActionForMode(action, currentMode)` — matrix lives in `config.mode.allowedActions`.
+2. `host.isValidSourceForMode(source, currentMode)` — matrix in `config.mode.allowedSources`.
+
+Defaults (per `generalFunctions/src/configs/rotatingMachine.json`):
+
+| Mode | Allowed actions | Allowed sources |
+|:---|:---|:---|
+| `auto` | `statuscheck, execmovement, execsequence, flowmovement, emergencystop, entermaintenance` | `parent, GUI, fysical` |
+| `virtualControl` | `statuscheck, execmovement, flowmovement, execsequence, emergencystop, exitmaintenance` | `GUI, fysical` |
+| `fysicalControl` | `statuscheck, emergencystop, entermaintenance, exitmaintenance` | `fysical` |
+
+A rejected action logs at warn (` is not allowed in mode ` or ` is not allowed in mode `) and short-circuits.
+
+---
+
+## Output ports
+
+| Port | Carries | Sample shape |
+|:---|:---|:---|
+| 0 (process) | Delta-compressed state snapshot — FSM state, predictions, drift, prediction health | `{topic, payload: {state, ctrl, flow.predicted.*, power.predicted.*, predictionQuality, ...}}` |
+| 1 (telemetry) | InfluxDB line-protocol payload (same fields as Port 0) | `rotatingMachine,id=pump_a state="operational",ctrl=60,flow_predicted_downstream_default=12.4,...` |
+| 2 (register / control) | `child.register` upward at init | `{topic: 'child.register', payload: {ref, softwareType, config}}` |
+
+Port-0 key shape is **`...`**. The trailing `` lets dashboards distinguish the same measurement type / position registered from different sources (real sensor vs `dashboard-sim`).
+
+See [EVOLV — Telemetry](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Telemetry) for the full InfluxDB layout.
+
+---
+
+## Event sources
+
+| Source | Where it fires | What it triggers |
+|:---|:---|:---|
+| `state.emitter` `'positionChange'` | `movementManager` setInterval during a move | `updatePosition()` — recompute predictions + Port 0 |
+| `state.emitter` `'stateChange'` | `stateManager.transitionTo` resolve | `_updateState()` — zero predictions if non-operational, refresh health, Port 0 |
+| `state.emitter` `'movementComplete'` | `state.moveTo` after a successful move | (subscribed but currently unused by orchestrator) |
+| `state.emitter` `'movementAborted'` | `state.moveTo` catch on aborted move | (subscribed but currently unused) |
+| Child measurement emitter | `child.measurements.emitter` per type / position | `pressureRouter.route` or `measurementHandlers.dispatch` |
+| Inbound `msg.topic` | Node-RED input wire | `commandRegistry` dispatch |
+| `setInterval(statusInterval = 1000)` | `BaseNodeAdapter` | Status badge re-render |
+
+No per-second tick on the domain itself. The movementManager's inner setInterval (50 ms by default) only runs while a position move is in flight.
+
+---
+
+## Where to start reading
+
+| If you're changing... | Read first |
+|:---|:---|
+| Curve loading, normalisation, fallback | `src/curves/{curveLoader, curveNormalizer, reverseCurve}.js` |
+| Per-machine + group predictors | `src/prediction/predictors.js`, `groupPredictors.js`, `predictionMath.js` |
+| Drift detection (EWMA, alignment) | `src/drift/{driftAssessor, healthRefresh, predictionHealth}.js` |
+| Pressure plumbing, virtual vs real preference | `src/pressure/{pressureInitialization, pressureRouter, pressureSelector, virtualChildren}.js` |
+| FSM bindings, setpoint, sequence orchestration | `src/state/{stateBindings, sequenceController}.js` + `generalFunctions/src/state/{state, stateManager, movementManager}.js` |
+| Sequence-abort token (the cooperating change for MGC's planner) | `generalFunctions/src/state/state.js` `abortCurrentMovement` + `src/state/sequenceController.js` `executeSequence` |
+| Per-type measurement handlers | `src/measurement/{measurementHandlers, childRegistrar}.js` |
+| Top-level action dispatch | `src/flow/flowController.js` |
+| `query.curves` / `query.cog` outputs | `src/display/workingCurves.js` |
+| Output shape, status badge | `src/io/output.js` |
+| Topic registration, payload validation | `src/commands/{index, handlers}.js` |
+
+---
+
+## Related pages
+
+| Page | Why |
+|:---|:---|
+| [Home](Home) | Intuitive overview |
+| [Reference — Contracts](Reference-Contracts) | Topic + config + child filters |
+| [Reference — Examples](Reference-Examples) | Shipped flows + debug recipes |
+| [Reference — Limitations](Reference-Limitations) | Known issues and open questions |
+| [machineGroupControl wiki](https://gitea.wbd-rd.nl/RnD/machineGroupControl/wiki/Home) | The grouped-control parent: planner, optimizer, rendezvous |
+| [EVOLV — Architecture](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Architecture) | Platform-wide three-tier pattern |
diff --git a/wiki/Reference-Contracts.md b/wiki/Reference-Contracts.md
new file mode 100644
index 0000000..9fbdbe8
--- /dev/null
+++ b/wiki/Reference-Contracts.md
@@ -0,0 +1,279 @@
+# Reference — Contracts
+
+
+
+> [!NOTE]
+> Full topic contract, configuration schema, and child-registration filters for `rotatingMachine`. Source of truth: `src/commands/index.js`, `src/specificClass.js` `configure()`, and the schema at `generalFunctions/src/configs/rotatingMachine.json`.
+>
+> For an intuitive overview, return to the [Home](Home).
+
+---
+
+## Topic contract
+
+The registry lives in `src/commands/index.js`. Each descriptor maps a canonical `msg.topic` to its handler; aliases emit a one-time deprecation warning the first time they fire.
+
+| Canonical topic | Aliases | Payload | Unit | Effect |
+|:---|:---|:---|:---|:---|
+| `set.mode` | `setMode` | `string` (`auto` / `virtualControl` / `fysicalControl`) | — | Switch operational mode. Each mode has its own allow-list of actions and sources. |
+| `cmd.startup` | — | any | — | Run the configured `startup` sequence (default `[starting, warmingup, operational]`). |
+| `cmd.shutdown` | — | any | — | Run the `shutdown` sequence. If currently `operational`, `executeSequence` first ramps the setpoint to 0 (interruptible). |
+| `cmd.estop` | `emergencystop` | any | — | Run the `emergencystop` sequence (default `[emergencystop, off]`). Reachable from every state. |
+| `set.setpoint` | `execMovement` | `{setpoint: number}` | control % (no `units` — convert has no `percent` measure) | Move to a control-axis setpoint via `state.moveTo`. |
+| `set.flow-setpoint` | `flowMovement` | `{setpoint: number}` or bare number | `volumeFlowRate` (default `m3/h`) | Convert to canonical m³/s, then to control % via `predictCtrl.y`, then `state.moveTo`. |
+| `data.simulate-measurement` | `simulateMeasurement` | `{asset: {type, unit}, value, position, childName?, childId?}` | type-specific | Inject a virtual sensor reading. The two virtual children (`dashboard-sim-upstream` / `-downstream`) auto-handle pressure; other types use the registering child's id. |
+| `query.curves` | `showWorkingCurves` | any | — | Reply on Port 0 with the current working curves (flow / power / efficiency). |
+| `query.cog` | `CoG` | any | — | Reply on Port 0 with the centre-of-gravity (CoG) point. |
+| `child.register` | `registerChild` | `string` (child node id) | — | Register a `measurement` child with this machine. Port 2 wiring does this automatically in normal flows. |
+| `execSequence` | — | `{action: "startup" \| "shutdown"}` | — | Legacy umbrella: demuxes `payload.action` to the canonical `cmd.startup` / `cmd.shutdown` handler. Marked `_legacy: true`; scheduled for removal. |
+
+### Mode / source / action allow-lists
+
+A topic that survives the registry still passes through `flowController.handle`:
+
+```js
+if (!host.isValidActionForMode(action, host.currentMode)) return;
+if (!host.isValidSourceForMode(source, host.currentMode)) return;
+```
+
+Defaults from the schema:
+
+| Mode | `allowedActions` | `allowedSources` |
+|:---|:---|:---|
+| `auto` | `statuscheck, execmovement, execsequence, flowmovement, emergencystop, entermaintenance` | `parent, GUI, fysical` |
+| `virtualControl` | `statuscheck, execmovement, flowmovement, execsequence, emergencystop, exitmaintenance` | `GUI, fysical` |
+| `fysicalControl` | `statuscheck, emergencystop, entermaintenance, exitmaintenance` | `fysical` |
+
+A rejected request logs at warn and short-circuits; nothing reaches the FSM.
+
+---
+
+## Data model — `getOutput()` shape
+
+Composed each tick by `src/io/output.js` `buildOutput()`. Delta-compressed: consumers see only the keys that changed.
+
+### Per-measurement keys
+
+For every `(type, variant, position)` stored in MeasurementContainer, the flattened output emits:
+
+```
+...
+```
+
+Position labels are normalised to lowercase in the keys (`atequipment`, `downstream`, `upstream`, `max`, `min`). The trailing `` is:
+
+| `` | When |
+|:---|:---|
+| `default` | The node's own predictions (flow / power / efficiency / Ncog). |
+| `dashboard-sim-upstream` / `dashboard-sim-downstream` | The two auto-registered virtual pressure children. |
+| The real child's `general.id` | When a registered measurement child wrote the value. |
+
+Sample keys (operational pump, simulated pressure):
+
+| Key | Type | Unit | Notes |
+|:---|:---|:---|:---|
+| `flow.predicted.downstream.default` | number | m³/h | Live predicted flow. |
+| `flow.predicted.atequipment.default` | number | m³/h | Same number, equipment-side label. |
+| `flow.predicted.max.default` / `.min.default` | number | m³/h | Curve envelope at the current `fDimension`. |
+| `power.predicted.atequipment.default` | number | kW | Predicted shaft power. |
+| `pressure.measured.upstream.dashboard-sim-upstream` | number | mbar | Last simulated suction pressure. |
+| `pressure.measured.downstream.dashboard-sim-downstream` | number | mbar | Last simulated discharge pressure. |
+| `temperature.measured.atequipment.dashboard-sim-upstream` | number | °C | Default 15°C until overwritten. |
+| `atmPressure.measured.atequipment.dashboard-sim-upstream` | number | Pa | Default 101325 Pa until overwritten. |
+
+### Scalar keys
+
+| Key | Type | Source | Notes |
+|:---|:---|:---|:---|
+| `state` | string | `host.state.getCurrentState()` | One of the FSM states (`idle`, `starting`, `warmingup`, …). |
+| `ctrl` | number | `host.state.getCurrentPosition()` | Control-axis position 0..100. |
+| `mode` | string | `host.currentMode` | `auto` / `virtualControl` / `fysicalControl`. |
+| `runtime` | number | `host.state.getRunTimeHours()` | Cumulative hours in active states. |
+| `moveTimeleft` | number | `host.state.getMoveTimeLeft()` | Seconds remaining on the current move (0 when idle). |
+| `maintenanceTime` | number | `host.state.getMaintenanceTimeHours()` | Cumulative hours in maintenance. |
+| `cog` / `NCog` / `NCogPercent` | number | `host.cog` etc. | CoG metric on the η curve. `NCog` 0..1; `NCogPercent` is `NCog * 100`, rounded to 2 dp. |
+| `effDistFromPeak` | number | `host.absDistFromPeak` | Absolute η distance to peak. |
+| `effRelDistFromPeak` | number | `host.relDistFromPeak` | Normalised 0..1; `undefined` when η band collapses. |
+| `predictionQuality` | string | `host.predictionHealth.quality` | `good` / `warming` / `degraded` / `invalid`. |
+| `predictionConfidence` | number | `host.predictionHealth.confidence` | 0..1, rounded to 3 dp. |
+| `predictionPressureSource` | string \| null | `host.predictionHealth.pressureSource` | `dashboard-sim` or a real child id; null until pressure landed. |
+| `predictionFlags` | array | `host.predictionHealth.flags` | Reason codes (e.g. `pressure_init_warming`). |
+| `pressureDriftLevel` | number | `host.pressureDrift.level` | 0..3. |
+| `pressureDriftSource` | string \| null | `host.pressureDrift.source` | Source whose drift is worst. |
+| `pressureDriftFlags` | array | `host.pressureDrift.flags` | `nominal` when no drift detected. |
+| `flowNrmse` / `flowLongTermNRMSD` / `flowImmediateLevel` / `flowLongTermLevel` / `flowDriftValid` | numbers / number / number / boolean | `host.flowDrift` | Only present once `flowDrift != null`. |
+| `powerNrmse` / `powerLongTermNRMSD` / `powerImmediateLevel` / `powerLongTermLevel` / `powerDriftValid` | same | `host.powerDrift` | Same. |
+
+### Status badge
+
+`buildStatusBadge` in `io/output.js`:
+
+```
+: % 💨 ⚡kW
+```
+
+State symbols (per `STATE_SYMBOLS` map):
+
+| State | Symbol | Fill |
+|:---|:---:|:---|
+| `off` | ⬛ | red |
+| `idle` | ⏸️ | blue |
+| `operational` | ⏵️ | green |
+| `starting` | ⏯️ | yellow |
+| `warmingup` | 🔄 | green |
+| `accelerating` | ⏩ | yellow |
+| `decelerating` | ⏪ | yellow |
+| `stopping` | ⏹️ | yellow |
+| `coolingdown` | ❄️ | yellow |
+| `maintenance` | 🔧 | grey |
+
+Pressure-not-initialised states (`operational`, `warmingup`, `accelerating`, `decelerating`) override the badge to a yellow ring `': pressure not initialized'` until at least one pressure source has been written.
+
+---
+
+## Configuration schema — editor form to config keys
+
+Source of truth: `generalFunctions/src/configs/rotatingMachine.json` plus `nodeClass.buildDomainConfig`.
+
+### General (`config.general`)
+
+| Form field | Config key | Default | Notes |
+|:---|:---|:---|:---|
+| Name | `general.name` | derived: `_` | Re-derived in `configure()`. |
+| (auto-assigned) | `general.id` | `null` | Node-RED node id. |
+| Default unit | `general.unit` | `l/s` (schema) / `m3/h` (nodeClass) | `buildDomainConfig` resolves `uiConfig.unit` via `convert` and overrides to a valid flow unit. |
+| Enable logging | `general.logging.enabled` | `true` | Master switch. |
+| Log level | `general.logging.logLevel` | `info` | `debug` / `info` / `warn` / `error`. |
+
+### Functionality (`config.functionality`)
+
+| Form field | Config key | Default | Notes |
+|:---|:---|:---|:---|
+| Position vs parent | `functionality.positionVsParent` | `atEquipment` | One of `atEquipment` / `upstream` / `downstream`. Used in the child-register payload that goes UP to MGC / pumpingStation. |
+| (hidden) | `functionality.softwareType` | `rotatingmachine` | Constant. |
+| (hidden) | `functionality.role` | `RotationalDeviceController` | Constant. |
+| Distance offset | `functionality.distance` | `null` | Optional spatial offset; populated when `hasDistance` is enabled. |
+| Distance unit | `functionality.distanceUnit` | `m` | |
+| Distance description | `functionality.distanceDescription` | `""` | Free-text. |
+
+### Asset (`config.asset`)
+
+Resolved derived metadata (supplier / category / type / allowed units) lives in `generalFunctions/datasets/assetData/rotatingmachine.json` keyed by `asset.model`. The editor's asset menu reads from that registry.
+
+| Form field | Config key | Default | Notes |
+|:---|:---|:---|:---|
+| Asset UUID | `asset.uuid` | `null` | Globally-unique identifier. |
+| Tag code | `asset.tagCode` | `null` | |
+| Tag number | `asset.tagNumber` | `null` | Legacy column. |
+| Geolocation | `asset.geoLocation` | `{x:0, y:0, z:0}` | |
+| Model | `asset.model` | `null` | **Required.** Resolves curve + supplier / type / allowed units via the registry. |
+| Deployment unit | `asset.unit` | `null` | **Required.** Must be a flow unit; soft-warned if not in the registry's recommended list for the model. |
+| Curve units | `asset.curveUnits` | `{pressure:'mbar', flow:'m3/h', power:'kW', control:'%'}` | Carried for curve normalisation. |
+| Accuracy | `asset.accuracy` | `null` | Optional sensor accuracy %. |
+| (derived) | `asset.machineCurve` | `{nq:{}, np:{}}` | Loaded from `loadModelCurve(model)`, then normalised. |
+
+> [!WARNING]
+> **Legacy fields removed.** `supplier`, `category`, and `assetType` are no longer node config — the registry derives them from the model. Flows saved before the AssetResolver refactor will throw a startup error with a clear migration message. Re-open the node, re-select the model from the asset menu, and save.
+
+### State times (`stateConfig.time`)
+
+Set on the state machine via `nodeClass.buildDomainConfig` from editor fields:
+
+| Form field | Config key | Default (schema) | Notes |
+|:---|:---|:---|:---|
+| Startup Time | `time.starting` | configured in s | Time spent in `starting` before transitioning to `warmingup`. |
+| Warmup Time | `time.warmingup` | configured in s | Time in `warmingup` — **non-interruptible** safety. |
+| Shutdown Time | `time.stopping` | configured in s | Time in `stopping`. |
+| Cooldown Time | `time.coolingdown` | configured in s | Time in `coolingdown` — **non-interruptible** safety. |
+
+### Movement (`stateConfig.movement`)
+
+| Form field | Config key | Default | Notes |
+|:---|:---|:---|:---|
+| Reaction Speed | `movement.speed` | configured in %/s | Controller ramp rate. E.g. `1` means 1%/s → setpoint 60 from idle reaches 60 in ~60 s. |
+| Movement Mode | `movement.mode` | `staticspeed` | `staticspeed` (linear ramp) or `dynspeed` (cubic ease-in-out). Both yield the same total duration; only the curve differs. |
+| (internal) | `movement.maxSpeed` | from schema | Hard cap honoured by `movementManager.getNormalizedSpeed`. |
+| (internal) | `movement.interval` | from schema | Inner-loop tick of the move animation (ms). |
+
+### Sequences (`config.sequences`)
+
+State-transition lists per sequence name. Defaults:
+
+| Sequence | States |
+|:---|:---|
+| `startup` | `[starting, warmingup, operational]` |
+| `shutdown` | `[stopping, coolingdown, idle]` |
+| `emergencystop` | `[emergencystop, off]` |
+| `boot` | `[idle, starting, warmingup, operational]` |
+| `entermaintenance` | `[stopping, coolingdown, idle, maintenance]` |
+| `exitmaintenance` | `[off, idle]` |
+
+Custom sequences are accepted as long as every step is a known FSM state and the transitions between them are allowed by `stateConfig.allowedTransitions`.
+
+### Output (`config.output`)
+
+| Form field | Config key | Default | Range | Notes |
+|:---|:---|:---|:---|:---|
+| Process Output | `output.process` | `process` | `process` / `json` / `csv` | Port-0 formatter. |
+| Database Output | `output.dbase` | `influxdb` | `influxdb` / `json` / `csv` | Port-1 formatter. |
+
+### Mode (`config.mode`)
+
+| Form field | Config key | Default | Range | Notes |
+|:---|:---|:---|:---|:---|
+| Mode | `mode.current` | `auto` | `auto` / `virtualControl` / `fysicalControl` | The active operational mode. |
+| (defaults) | `mode.allowedActions.` | see [Architecture](Reference-Architecture#mode--source-allow-lists) | enforced by `flowController.handle` |
+| (defaults) | `mode.allowedSources.` | see [Architecture](Reference-Architecture#mode--source-allow-lists) | enforced by `flowController.handle` |
+
+### Unit policy
+
+Source: `src/specificClass.js` lines 36–41.
+
+| Quantity | Canonical (internal) | Output (rendered) | Curve (supplier) | Required-unit |
+|:---|:---|:---|:---|:---:|
+| Pressure | `Pa` | `mbar` | `mbar` | ✓ |
+| Atmospheric pressure | `Pa` | `Pa` | — | ✓ |
+| Flow | `m3/s` | `m3/h` | `m3/h` | ✓ |
+| Power | `W` | `kW` | `kW` | ✓ |
+| Temperature | `K` | `°C` | — | ✓ |
+| Control | — | — | `%` | — |
+
+`requireUnitForTypes` means MeasurementContainer rejects writes that omit `unit` for these types.
+
+---
+
+## Child registration
+
+Source: `src/measurement/childRegistrar.js` `registerMeasurementChild`. The registrar reads `asset.type` and `positionVsParent` from the child's config and subscribes to `.measured.` on the child's measurement emitter.
+
+| Software type | Filter | Wired to | Side-effect |
+|:---|:---|:---|:---|
+| `measurement` | `asset.type='pressure', position=upstream` | `pressureRouter.route('upstream', value, ctx)` | Stored as upstream pressure; refresh prediction + drift. `pressureInitialization` tracks readiness. |
+| `measurement` | `asset.type='pressure', position=downstream` | `pressureRouter.route('downstream', value, ctx)` | Same on the discharge side. |
+| `measurement` | `asset.type='flow', position=*` | `measurementHandlers.updateMeasuredFlow` | Stored; drift assessed against predicted. |
+| `measurement` | `asset.type='power', position=atEquipment` | `measurementHandlers.updateMeasuredPower` | Stored; drift assessed against predicted. |
+| `measurement` | `asset.type='temperature', position=*` | `measurementHandlers.updateMeasuredTemperature` | Stored; surfaced on Port 0. |
+
+### Virtual pressure children — auto-registered
+
+At startup `specificClass` registers two `measurement`-typed children:
+
+| Child id | Position | Default value | Use |
+|:---|:---|:---|:---|
+| `dashboard-sim-upstream` | `upstream` | 0 mbar | Receives `data.simulate-measurement` payloads with position `upstream`. |
+| `dashboard-sim-downstream` | `downstream` | 0 mbar | Same for `downstream`. |
+
+`pressureSelector` prefers a real registered child over the virtuals once one shows up — the virtuals keep listening so dashboards can still inject sim values during real-pressure outages.
+
+---
+
+## Related pages
+
+| Page | Why |
+|:---|:---|
+| [Home](Home) | Intuitive overview |
+| [Reference — Architecture](Reference-Architecture) | Code map, FSM, prediction + drift pipeline |
+| [Reference — Examples](Reference-Examples) | Shipped flows + debug recipes |
+| [Reference — Limitations](Reference-Limitations) | Known issues and open questions |
+| [EVOLV — Topic Conventions](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions) | Platform-wide topic rules |
+| [EVOLV — Telemetry](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Telemetry) | Port 0 / 1 / 2 InfluxDB layout |
diff --git a/wiki/Reference-Examples.md b/wiki/Reference-Examples.md
new file mode 100644
index 0000000..7be9c45
--- /dev/null
+++ b/wiki/Reference-Examples.md
@@ -0,0 +1,169 @@
+# Reference — Examples
+
+
+
+> [!NOTE]
+> Every example flow shipped under `nodes/rotatingMachine/examples/`, plus how to load them, what they show, and the debug recipes that go with them. Live source: `nodes/rotatingMachine/examples/`.
+
+---
+
+## Shipped examples
+
+| File | Tier | Dependencies | What it shows |
+|:---|:---:|:---|:---|
+| `01 - Basic Manual Control.json` | 1 | EVOLV only | Single pump driven by inject buttons — mode switching, startup / shutdown / e-stop, control-% and flow-unit setpoints, simulated pressures, maintenance enter / leave. Debug taps on all three ports. |
+| `02 - Integration with Machine Group.json` | 2 | EVOLV only | Parent-child demo — one `machineGroupControl` with 2 `rotatingMachine` children. Auto-registration via Port 2 on deploy. Per-pump simulated pressures. |
+| `03 - Dashboard Visualization.json` | 3 | EVOLV + `@flowfuse/node-red-dashboard` | FlowFuse charts: flow / power / pressure trends, status panel, per-pump controls. |
+
+Three legacy files (`basic.flow.json`, `integration.flow.json`, `edge.flow.json`) are kept until the new Tier-2 has been fully Docker-validated; they predate the AssetResolver refactor and may need re-save in the editor before they deploy.
+
+---
+
+## Loading a flow
+
+### Via the editor
+
+1. Open the Node-RED editor at `http://localhost:1880`.
+2. Menu → Import → drag the JSON file.
+3. Click Deploy.
+
+(The numbered files contain spaces; in the editor's import dialog the filename is purely cosmetic.)
+
+### Via the Admin API
+
+```bash
+curl -X POST -H 'Content-Type: application/json' \
+ --data @"nodes/rotatingMachine/examples/01 - Basic Manual Control.json" \
+ http://localhost:1880/flows
+```
+
+---
+
+## Example 01 — Basic Manual Control
+
+Single-pump flow with one of every input you'd ever send. Validated against a live Node-RED instance (2026-03-05).
+
+### Nodes on the tab
+
+| Type | Purpose |
+|:---|:---|
+| `comment` | Tab header / driver-group labels |
+| `inject` × 9 | Mode (auto / virtualControl), startup, shutdown, e-stop, setpoint = 30 / 60 / 100 %, simulated upstream + downstream pressures, simulate flow / power for drift |
+| `rotatingMachine` | The unit under test |
+| `debug` × 3 | Port 0 (process), Port 1 (telemetry), Port 2 (registration) |
+
+### What to do after deploy
+
+1. Click the two pressure simulations (upstream = 0 mbar, downstream = 1100 mbar). Once both land, `predictionPressureSource` flips from `null` to `dashboard-sim` and `predictionFlags` drops the `pressure_init_warming` flag.
+2. Click `set.mode = virtualControl` so the GUI source is allowed.
+3. Click `cmd.startup`. Watch Port 0 in the debug pane: `state` walks `idle → starting → warmingup → operational`. `runtime` starts accumulating.
+4. Click `set.setpoint = 60` (control %). `state` goes `operational → accelerating → operational`; `ctrl` rises from 0 to 60 at the configured `Reaction Speed`. `flow.predicted.downstream.default` and `power.predicted.atequipment.default` update at every position tick.
+5. Click `set.flow-setpoint = {value: 80, unit: 'm3/h'}` — same path, but the setpoint is a flow value; the node converts via `predictCtrl` to a control %.
+6. Click `cmd.shutdown`. State: `operational → decelerating → stopping → coolingdown → idle`. The ramp-to-zero step is interruptible; the subsequent transitions are timed by `time.stopping` and `time.coolingdown`.
+
+> [!IMPORTANT]
+> **GIF needed.** Demo recording of steps 1–6 + the status badge progression. Save as `wiki/_partial-gifs/rotatingMachine/01-basic-demo.gif`, target ≤ 1 MB after `gifsicle -O3 --lossy=80`.
+
+### Try the residue handler
+
+After the pump reaches `operational` at 60 %:
+
+1. Send `set.setpoint = 20`. `state` goes `operational → decelerating → …`.
+2. While `decelerating`, send `set.setpoint = 80`.
+3. `state.moveTo` sees the residue, transitions back to `operational` synchronously, then ramps up to 80. No setpoint is lost.
+
+This is the same mechanism the MGC planner relies on for fast retargets.
+
+### Try the sequence-abort token
+
+After the pump reaches `operational` at 60 %, simulate the Scenario-5 race:
+
+1. Send `cmd.shutdown`. The pump begins ramping to zero.
+2. *Within the ramp window*, send `set.setpoint = 60`. The new setpoint's residue-handler claims the FSM back to `operational`.
+3. Watch the log: instead of the shutdown's for-loop continuing through `stopping → coolingdown → idle`, you'll see `Sequence 'shutdown' interrupted during ramp-down by external abort; not entering shutdown loop.`
+
+Without the token (pre-2026-05-15), the pump would have ended at `idle` despite the new setpoint — with `delayedMove = 60` sitting unused.
+
+---
+
+## Example 02 — Integration with Machine Group
+
+> [!IMPORTANT]
+> **Screenshot needed.** Editor capture of `02 - Integration with Machine Group.json`. Save as `wiki/_partial-screenshots/rotatingMachine/02-integration.png`. Replace this callout with the image link.
+
+One MGC + two rotatingMachine children. Demonstrates:
+
+- Auto-registration via Port 2 at deploy (each pump's `child.register` reaches the MGC; no manual wiring needed).
+- Independent per-pump controls (the injects still target each pump's input by id).
+- Group-level aggregation: MGC's Port 0 sums the children's predicted flow + power into the group aggregate.
+
+The MGC planner is exercised when MGC's `set.demand` fires (not in this example by default; add an inject if you want to see it).
+
+---
+
+## Example 03 — Dashboard Visualization
+
+> [!IMPORTANT]
+> **Screenshots needed.** Two captures: the editor tab and the rendered dashboard. Save as `wiki/_partial-screenshots/rotatingMachine/03-dashboard-editor.png` and `04-dashboard-rendered.png`.
+
+A single pump on a FlowFuse Dashboard 2.0 page with:
+
+- Control buttons (mode, startup, shutdown, e-stop)
+- A setpoint slider
+- Live status (state badge, ctrl%, predicted flow / power / efficiency)
+- Trend charts: flow, power, pressure, drift level
+
+Required: `@flowfuse/node-red-dashboard` installed in the Node-RED instance.
+
+---
+
+## Docker compose snippet
+
+To bring up Node-RED + InfluxDB with EVOLV nodes pre-loaded:
+
+```yaml
+# docker-compose.yml (extract)
+services:
+ nodered:
+ build: ./docker/nodered
+ ports: ['1880:1880']
+ volumes:
+ - ./docker/nodered/data:/data/evolv
+ influxdb:
+ image: influxdb:2.7
+ ports: ['8086:8086']
+```
+
+Full file: [EVOLV/docker-compose.yml](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/docker-compose.yml).
+
+---
+
+## Debug recipes
+
+| Symptom | First thing to check | Where to look |
+|:---|:---|:---|
+| Editor throws `legacy asset field(s) [supplier]` on deploy | Flow predates the AssetResolver refactor. Re-open the node, pick the model from the asset menu, save. The registry derives supplier / category / type. | `src/nodeClass.js` `_rejectLegacyAssetFields`. |
+| `state` stuck on `idle` after `cmd.startup` | The action isn't allowed for this mode / source combination. Check `flowController` warn log for ` is not allowed in mode ` or ` is not allowed in mode `. | `_setupState`, `isValidSourceForMode`, `isValidActionForMode`. |
+| `flow.predicted.*` reads `0` or `NaN` | Pressure hasn't initialised. `predictionFlags` will include `pressure_init_warming`. Inject pressure via `data.simulate-measurement` or wire real measurement children. | `getMeasuredPressure` + `pressureSelector`. |
+| `predictionQuality: 'invalid'` from startup | Curve normalisation failed — null predictors installed. Look for `Curve normalization failed for model …` in the log. The asset / model is unrecognised, the unit isn't a flow unit, or the registry entry is missing. | `_setupCurves`. |
+| Drift level stays at `3` after startup | Fewer than `minSamplesForLongTerm = 10` paired samples have landed. Wait ~10 ticks; the level falls automatically. | `driftProfiles.minSamplesForLongTerm`. |
+| `cmd.estop` and then the pump won't restart | Allowed transitions out of `emergencystop` are `idle` / `off` / `maintenance`. Send `cmd.shutdown` to drop into `idle`, then `cmd.startup`. | `stateConfig.allowedTransitions.emergencystop`. |
+| Position bounces near the target | `dynspeed` (cubic ease-in-out) can overshoot at high speed. Try `staticspeed` (linear). Both modes have the same total duration. | `movement.mode`. |
+| Pump still drifts to `idle` after a mid-shutdown re-engage | Verify the submodule is at `394a972` or newer — the sequence-abort token in `state.js` + `sequenceController.js` is what closes that race. | `state.sequenceAbortToken`. |
+| `data.simulate-measurement` payloads aren't reflected on Port 0 | Payload shape: `{asset: {type: 'pressure', unit: 'mbar'}, value: 1100, position: 'downstream', childId: 'dashboard-sim-downstream'}`. Missing `asset.type` or `position` gets a `Unsupported simulateMeasurement type:` warn and is dropped. | `measurementHandlers.updateSimulatedMeasurement`. |
+| Per-pump Port 0 key names differ from what your dashboard expects | rotatingMachine uses `...` (e.g. `flow.predicted.downstream.default`). MGC uses `__`. Don't mix them. | `io/output.js`, `MeasurementContainer.getFlattenedOutput`. |
+
+> Never ship `enableLog: 'debug'` in a demo — fills the container log within seconds and obscures real errors.
+
+---
+
+## Related pages
+
+| Page | Why |
+|:---|:---|
+| [Home](Home) | Intuitive overview |
+| [Reference — Contracts](Reference-Contracts) | Topic + config + child filters |
+| [Reference — Architecture](Reference-Architecture) | Code map, FSM, prediction + drift pipeline |
+| [Reference — Limitations](Reference-Limitations) | Known issues and open questions |
+| [machineGroupControl — Examples](https://gitea.wbd-rd.nl/RnD/machineGroupControl/wiki/Reference-Examples) | Group-control demo flows |
+| [EVOLV — Topology Patterns](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topology-Patterns) | Where rotatingMachine fits in a larger plant |
diff --git a/wiki/Reference-Limitations.md b/wiki/Reference-Limitations.md
new file mode 100644
index 0000000..27f38b8
--- /dev/null
+++ b/wiki/Reference-Limitations.md
@@ -0,0 +1,105 @@
+# Reference — Limitations
+
+
+
+> [!NOTE]
+> What `rotatingMachine` does not do, current rough edges, and open questions. Open items live in `.agents/improvements/IMPROVEMENTS_BACKLOG.md` in the superproject.
+
+---
+
+## When you would not use this node
+
+| Scenario | Use instead |
+|:---|:---|
+| A passive non-return / check valve (no motor) | `valve` — no curve, no FSM-driven motor. |
+| A valve actuator (motorised, no characteristic curve) | `valve` (and `valveGroupControl` if grouped). |
+| A group of 2 + pumps load-sharing on a header | `machineGroupControl` — instantiate this as a child. |
+| A curve-less asset | Predictions degrade to zero, drift becomes meaningless, status badge falls into `predictionQuality: 'invalid'`. There is no fallback model. |
+| A compressor with significant gas compressibility | Predictor uses an incompressible-flow curve; output is qualitatively right but quantitatively biased. Tracked. |
+
+---
+
+## Known limitations
+
+### Single-side pressure degrades silently
+
+`pressureSelector.getMeasuredPressure` accepts only-upstream or only-downstream readings as a fallback when the differential is unknown. It logs a warn (`Using downstream pressure only for prediction: …. Prediction accuracy is degraded; inject upstream pressure too.`) but proceeds. The predictor uses the absolute pressure as a surrogate differential, which can materially bias flow predictions under varying suction conditions. The warn is one-shot per state transition, not per tick — it can be missed in long-running deployments. Tracked.
+
+### Multi-parent registration
+
+`childRegistrationUtils` accepts registration under multiple parents. The pump emits child-register messages to each, and parents listen in parallel. Teardown ordering (parent gone first vs pump gone first) is not test-covered; observed behaviour in production is "fine, mostly". If you wire one pump to two MGCs and remove one MGC mid-deployment, the pump's listener set may keep a stale reference. Open question.
+
+### `data.simulate-measurement` doesn't clear stale values
+
+If you toggle a virtual pressure off (stop sending the inject), the last-known value persists in the MeasurementContainer. There is no TTL and no explicit clear topic. Workaround: send `value: null` or `0` explicitly. Tracked.
+
+### `execSequence` legacy umbrella
+
+The `execSequence` topic (with `payload.action = "startup" | "shutdown"`) is kept alive for legacy flows. The handler demuxes to the canonical topic; both emit a one-time deprecation warning. Scheduled for removal in a later phase. Use `cmd.startup` / `cmd.shutdown` instead.
+
+### Drift confidence collapses on long pressure-source outages
+
+`predictionHealth.refresh` reduces `predictionConfidence` to 0 when no pressure source has produced a reading in > 30 s. The quality string flips to `invalid` — downstream consumers should treat this as "predictor is offline, ignore values" rather than "predictor is broken". The recovery is automatic: as soon as a pressure measurement lands, health climbs back. Open question whether to model this as a discrete "stale" quality state instead.
+
+### `state` stays in residue after a routine abort
+
+`abortCurrentMovement` with default options (the kind MGC fires) does **not** auto-transition the FSM back to `operational`. The pump stays parked in `accelerating` / `decelerating` until the next `moveTo` arrives — at which point the residue handler in `state.moveTo` runs the transition synchronously. By design (a previous version auto-transitioned and created a bounce loop where every tick aborted, returned, re-moved, aborted again). See the comment in `state.js` `moveTo` line 76 for the historical detail.
+
+### Editor cosmetics don't reflect `asset` derivation
+
+The editor form still has visual sections for supplier / category / type even though the registry derives them. They're read-only and informational; some fields render as blank until you select a model. Cosmetic; the registry is the source of truth.
+
+---
+
+## Open questions (tracked)
+
+| Question | Where it lives |
+|:---|:---|
+| Should the predictor use an explicit "stale" quality state instead of collapsing to `invalid` when pressure data dries up? | Internal — not yet ticketed |
+| Multi-parent teardown ordering | Internal |
+| Add an explicit `data.clear-simulated-measurement` topic for sim cleanup | Internal |
+| Compressor / gas-flow curve handling | Internal (long-term) |
+| Phase 7 removal of `execSequence` umbrella + legacy aliases | Internal |
+| Curve loader robustness: warn / refuse mismatched curve units instead of best-effort normalising | `OPEN_QUESTIONS.md` (rotatingMachine entry) |
+
+---
+
+## Migration notes
+
+### From pre-AssetResolver
+
+Old flows saved with `supplier`, `category`, or `assetType` fields will throw on deploy:
+
+```
+rotatingMachine: legacy asset field(s) [supplier, category] are saved on this node.
+After the AssetResolver refactor these are derived from the model id.
+Open the node in the editor, re-select the model, and save to migrate.
+```
+
+The fix is mechanical: open each rotatingMachine node, re-pick the model from the asset menu, save. No data is lost — the registry has the same supplier / category / type the old flow carried.
+
+### From pre-sequence-abort-token
+
+Before 2026-05-15 a mid-decel re-engage was a race — sometimes the shutdown's for-loop won and parked the pump at `idle` with an orphaned `delayedMove`. With the `sequenceAbortToken` mechanism in `state.js` + `sequenceController.js` (from `394a972` onward), the new-dispatch's `abortCurrentMovement` always wins: the shutdown's for-loop breaks out before its next transition.
+
+If you have an integration test that relied on the older "shutdown always completes" behaviour, expect to see `Sequence 'shutdown' interrupted ... by external abort` warnings instead. That's the intended new state.
+
+### From `setpoint` topic name (pre-canonical)
+
+The old `setpoint` topic without a `set.` prefix has been retired. Use `set.setpoint` (alias `execMovement`) for control-% setpoints and `set.flow-setpoint` (alias `flowMovement`) for flow setpoints.
+
+### From `execMovement` payload shape change
+
+Legacy payloads were `{source, action: "execMovement", setpoint: number}`. The current shape is the same minus `action` (the handler dispatches via topic). Both are accepted.
+
+---
+
+## Related pages
+
+| Page | Why |
+|:---|:---|
+| [Home](Home) | Intuitive overview |
+| [Reference — Contracts](Reference-Contracts) | Topic + config + child filters (alias map at the end) |
+| [Reference — Architecture](Reference-Architecture) | Code map, FSM (including sequence-abort token), prediction + drift |
+| [Reference — Examples](Reference-Examples) | Shipped flows + debug recipes |
+| [machineGroupControl — Limitations](https://gitea.wbd-rd.nl/RnD/machineGroupControl/wiki/Reference-Limitations) | Where the parent's planner currently bypasses priority mode |
diff --git a/wiki/_Sidebar.md b/wiki/_Sidebar.md
new file mode 100644
index 0000000..6df5641
--- /dev/null
+++ b/wiki/_Sidebar.md
@@ -0,0 +1,19 @@
+### rotatingMachine
+
+- [Home](Home)
+
+**Reference**
+
+- [Contracts](Reference-Contracts)
+- [Architecture](Reference-Architecture)
+- [Examples](Reference-Examples)
+- [Limitations](Reference-Limitations)
+
+**Related**
+
+- [EVOLV master wiki](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Home)
+- [machineGroupControl wiki](https://gitea.wbd-rd.nl/RnD/machineGroupControl/wiki/Home)
+- [pumpingStation wiki](https://gitea.wbd-rd.nl/RnD/pumpingStation/wiki/Home)
+- [Topology Patterns](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topology-Patterns)
+- [Topic Conventions](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions)
+- [Telemetry](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Telemetry)