--- title: rotatingMachine — User Manual node: rotatingMachine updated: 2026-04-13 status: trial-ready --- # rotatingMachine — User Manual The `rotatingMachine` node models a single pump, compressor, or blower. It runs an S88-style state machine, predicts flow and power from a supplier curve, and publishes process and telemetry data every second. It is the atomic control module beneath `machineGroupControl` and `pumpingStation`. This manual is the operator-facing reference. For architecture and the 3-tier code layout see [Node Architecture](../../architecture/node-architecture.md); for curve theory see [3D Pump Curves](../../architecture/3d-pump-curves.md). ## At a glance | Item | Value | |---|---| | Node category | EVOLV | | Inputs | 1 (message-driven) | | Outputs | 3 — `process` / `dbase` / `parent` | | Tick period | 1 s | | State machine | 10 states (S88) | | Predictions | curve-backed (nq flow, np power, reversed nq for ctrl) | | Canonical units | Pa, m³/s, W, K | ## Editor configuration | Field | Default | Meaning | |---|---|---| | **Reaction Speed** | `1` | Ramp rate in controller-position units per second. `1` = 1 %/s. | | **Startup Time** | `0` | Seconds in the `starting` state. | | **Warmup Time** | `0` | Seconds in the protected `warmingup` state. | | **Shutdown Time** | `0` | Seconds in the `stopping` state. | | **Cooldown Time** | `0` | Seconds in the protected `coolingdown` state. | | **Movement Mode** | `staticspeed` | `staticspeed` = linear ramp; `dynspeed` = ease-in/out. | | **Process Output** | `process` | Port 0 payload format: `process` (delta-compressed) / `json` / `csv`. | | **Database Output** | `influxdb` | Port 1 payload format: `influxdb` line protocol / `json` / `csv`. | | **Asset** (menu) | — | Supplier, category, model (must match a curve file in `generalFunctions/datasets`), output flow unit, curve units. | | **Logger** (menu) | `info`, enabled | Log level and toggle. | | **Position** (menu) | `atEquipment` | `upstream` / `atEquipment` / `downstream` relative to parent. Icon and optional distance offset. | > **Tip.** With `Reaction Speed = 1` and `Set 60%` from idle, the controller takes ~60 s to reach 60 %. Scale `Reaction Speed` up to emulate a faster actuator (e.g. `20` gives 1 second per 20 % = 3 s to reach 60 %). ## Input topics Every command enters on the single input port. `msg.topic` selects the handler; `msg.payload` carries the arguments. ### `setMode` ```json { "topic": "setMode", "payload": "virtualControl" } ``` Valid values: `auto`, `virtualControl`, `fysicalControl`. The current mode gates *which source* may issue *which action* (mode/action/source policy lives in `generalFunctions/src/configs/rotatingMachine.json`). ### `execSequence` ```json { "topic": "execSequence", "payload": { "source": "GUI", "action": "execSequence", "parameter": "startup" } } ``` `parameter` values: `startup`, `shutdown`, `entermaintenance`, `exitmaintenance`. Case is normalized. If a `shutdown` is issued while the machine is mid-ramp (`accelerating` / `decelerating`), the active movement is aborted and the shutdown proceeds as soon as the FSM has returned to `operational`. ### `execMovement` ```json { "topic": "execMovement", "payload": { "source": "GUI", "action": "execMovement", "setpoint": 60 } } ``` `setpoint` is expressed in controller units (0–100 %). ### `flowMovement` ```json { "topic": "flowMovement", "payload": { "source": "parent", "action": "flowMovement", "setpoint": 150 } } ``` `setpoint` is expressed in the configured **output flow unit** (e.g. m³/h). The node converts flow → controller-% via the reversed nq curve and then drives `execMovement`. ### `emergencystop` ```json { "topic": "emergencystop", "payload": { "source": "GUI", "action": "emergencystop" } } ``` Aborts any active movement, runs the `emergencystop` → `off` transition. Allowed from every active state. Case-insensitive. ### `simulateMeasurement` Inject a dashboard-side measurement without wiring a sensor child. Useful for validation, smoke tests, demo flows. ```json { "topic": "simulateMeasurement", "payload": { "type": "pressure", "position": "upstream", "value": 200, "unit": "mbar" } } ``` `type`: `pressure` / `flow` / `temperature` / `power`. `unit` is required and must be convertible to the canonical unit for the type. ### Diagnostics - `showWorkingCurves` — snapshot of current curve slices + computed metrics; reply on port 0. - `CoG` — current centre-of-gravity (peak efficiency point) indicators; reply on port 0. ### `registerChild` Internal. Sensor children (typically `measurement` nodes) send this to bind themselves to the machine. The machine also emits one on port 2 shortly after deploy so a parent group/station can register it. ## Output ports ### Port 0 — process data Delta-compressed payload. Only *changed* fields are emitted each tick. Keys use a **4-segment** format: ``` ... ``` Examples: | Key | Meaning | |---|---| | `flow.predicted.downstream.default` | predicted flow at discharge | | `flow.predicted.atequipment.default` | predicted flow at equipment | | `power.predicted.atequipment.default` | predicted electrical power draw | | `pressure.measured.downstream.dashboard-sim-downstream` | simulated discharge pressure | | `pressure.measured.upstream.` | real upstream sensor reading | | `state` | current FSM state | | `mode` | current mode | | `ctrl` | current controller position (0–100 %) | | `NCog` / `cog` | normalized / absolute centre-of-gravity | | `runtime` | cumulative operational hours | Consumers must cache and merge deltas. The example flow `01 - Basic Manual Control.json` includes a function node that does exactly this — reuse its logic in your own flows. ### Port 1 — dbase (InfluxDB) InfluxDB line-protocol payload formatted for the `telemetry` bucket. Tags are low-cardinality fields (node name, machine type); measurements are numeric values. See the [InfluxDB Schema Design](../../concepts/influxdb-schema-design.md) page for the full tag/field contract. ### Port 2 — parent `{ topic: "registerChild", payload: , positionVsParent }` — emitted once ~180 ms after deploy so a downstream parent group can discover this machine. Subsequent commands and data flow through the parent's input port. ## State machine ``` ┌────────────────────────────┐ │ operational │◄────┐ └────┬──────────┬────────┬────┘ │ │ │ │ │ execMovement │ │ │ │ execMovement │ │ │ │ ▼ ▼ ▼ ▼ │ accelerating decelerating │ emergencystop ─► off │ │ │ └─── (abort)─┘ │ │ │ ┌────▼──────────▼────┐ │ stopping │ └────────┬─────────────┘ │ coolingdown │ idle │ starting │ warmingup │ (operational) ``` Protected states (cannot be aborted by a new command): `warmingup`, `coolingdown`. Interruptible states: `accelerating`, `decelerating`. A `shutdown` or `emergencystop` issued during a ramp aborts the ramp and drives the FSM correctly to `idle` / `off`. Active states (contribute to `runtime`): `operational`, `starting`, `warmingup`, `accelerating`, `decelerating`. ## Predictions and pressure Flow and power are curve-backed. The curve set is indexed by the differential pressure across the machine: 1. Best: both upstream and downstream pressures present → real Δp. 2. Degraded: only one side present → falls back to that side with a warn. 3. Minimum: no pressure → `fDimension = 0`; flow and power predictions use the lowest curve slice and will look unrealistic. Pressure sources are resolved in priority order **real sensor child > virtual dashboard child > aggregated fallback**. Real-child values always win. Predictions are only emitted while the FSM is in an active state (`operational`, `starting`, `warmingup`, `accelerating`, `decelerating`). In `idle`, `stopping`, `coolingdown`, `off`, `maintenance` the outputs are clamped to zero. ### Supported curves and verification | Model | Pressure envelope | Flow envelope | Power envelope | |---|---|---|---| | `hidrostal-H05K-S03R` | 700 – 3900 mbar (33 slices) | 9.5 – 227 m³/h | 8.2 – 65.1 kW | | `hidrostal-C5-D03R-SHN1` | 400 – 2900 mbar (26 slices) | 6.4 – 52.5 m³/h | 0.55 – 31.5 kW | Both curves are covered by unit tests (`test/integration/curve-prediction.integration.test.js`) and a live E2E benchmark (`test/e2e/curve-prediction-benchmark.py`) that sweeps each pump through its own pressure × controller envelope. Last green run: **2026-04-13** — 12/12 samples per curve inside envelope, ctrl-monotonic, inverse-pressure monotonic. > **Pressure out of envelope is not clamped.** If a measured pressure falls *below* the curve's minimum slice, the node extrapolates and may produce implausibly large flow values (e.g. H05K at 400 mbar, ctrl 20 % → flow ≈ 30 000 m³/h; real envelope max is 227). Use realistic sensor ranges on your pressure `measurement` children. ## Example flows In the editor: **Import ▸ Examples ▸ EVOLV ▸ rotatingMachine**. - `01 - Basic Manual Control.json` — single machine, inject-only. Good for smoke-testing a node installation. - `02 - Integration with Machine Group.json` — `machineGroupControl` with two pumps as children. Good for verifying registration and parent orchestration. - `03 - Dashboard Visualization.json` — FlowFuse dashboard with live charts. Depends on `@flowfuse/node-red-dashboard`. ## Troubleshooting | Symptom | Likely cause | Fix | |---|---|---| | Editor says `pressure not initialized`, status ring is yellow | No pressure child wired yet and no simulated pressure injected. | Inject a `simulateMeasurement` of type `pressure` (both sides preferred) or wire a `measurement` child. | | Predictions are enormous at `ctrl = 0 %` | At near-zero controller position with high backpressure, the intercept of the curve gives a nominally-nonzero flow. This is a curve-data artefact, not a runtime bug. | Confirm the curve with Rene / supplier data. For a conservative prediction use a lower `Reaction Speed` or constrain `setpoint` ≥ 10 %. | | "Transition aborted" / "Movement aborted" in logs | Expected during `shutdown` / `emergencystop` issued during a ramp — the fix path intentionally aborts the active move. | None — informational only. | | Status bar shows `pressure not initialized` even after inject | `simulateMeasurement` payload missing `unit` or with a non-convertible value. | Include `unit` (e.g. `"mbar"`) and a finite number in `value`. | | Shutdown does nothing and no error | Machine is in `warmingup` or `coolingdown` (protected). | Wait for the phase to complete (≤ configured seconds) and retry. | ## Running it locally ```bash git clone --recurse-submodules https://gitea.wbd-rd.nl/RnD/EVOLV.git cd EVOLV docker compose up -d # Node-RED: http://localhost:1880 InfluxDB: :8086 Grafana: :3000 ``` Then in Node-RED: **Import ▸ Examples ▸ EVOLV ▸ rotatingMachine ▸ 01 - Basic Manual Control**. ## Testing ```bash cd nodes/rotatingMachine npm test ``` Unit tests (79) cover construction, mode gating, sequences, interruptible movement, emergency stop, shutdown, efficiency/CoG, pressure initialization, output formatting, listener cleanup. See also `examples/README.md` for the flow-level test matrix. ## Production status See the project memory entry `node_rotatingMachine.md` for the latest benchmarks and wishlist. Trial-ready as of 2026-04-13 following the interruptibility + schema-sync fixes documented in [session 2026-04-13](../../sessions/2026-04-13-rotatingMachine-trial-ready.md).