diff --git a/nodes/generalFunctions b/nodes/generalFunctions index 024db55..75d16c6 160000 --- a/nodes/generalFunctions +++ b/nodes/generalFunctions @@ -1 +1 @@ -Subproject commit 024db5533a190326ac2ef6a4502f4f87c51e6755 +Subproject commit 75d16c620a1b2d5d4f91b849e38fc2a8c7d7bc61 diff --git a/nodes/rotatingMachine b/nodes/rotatingMachine index 07af7ce..17b8887 160000 --- a/nodes/rotatingMachine +++ b/nodes/rotatingMachine @@ -1 +1 @@ -Subproject commit 07af7cef405ae741301ed8d7ceaeb4b09bafc202 +Subproject commit 17b88870bb1654a03a19c5e5310a5d002417bc6e diff --git a/wiki/index.md b/wiki/index.md index 0386407..9f49eb4 100644 --- a/wiki/index.md +++ b/wiki/index.md @@ -1,6 +1,6 @@ --- title: Wiki Index -updated: 2026-04-07 +updated: 2026-04-13 --- # EVOLV Project Wiki Index @@ -37,6 +37,7 @@ updated: 2026-04-07 - [Open Issues (2026-03)](findings/open-issues-2026-03.md) — diffuser, monster refactor, ML relocation, etc. ## Manuals +- [rotatingMachine User Manual](manuals/nodes/rotatingMachine.md) — inputs, outputs, state machine, examples - [FlowFuse Dashboard Layout](manuals/node-red/flowfuse-dashboard-layout-manual.md) - [FlowFuse Widget Catalog](manuals/node-red/flowfuse-widgets-catalog.md) - [Node-RED Function Patterns](manuals/node-red/function-node-patterns.md) diff --git a/wiki/manuals/nodes/rotatingMachine.md b/wiki/manuals/nodes/rotatingMachine.md new file mode 100644 index 0000000..a6927c3 --- /dev/null +++ b/wiki/manuals/nodes/rotatingMachine.md @@ -0,0 +1,247 @@ +--- +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). diff --git a/wiki/sessions/2026-04-13-rotatingMachine-trial-ready.md b/wiki/sessions/2026-04-13-rotatingMachine-trial-ready.md new file mode 100644 index 0000000..17d9030 --- /dev/null +++ b/wiki/sessions/2026-04-13-rotatingMachine-trial-ready.md @@ -0,0 +1,134 @@ +--- +title: "Session: rotatingMachine trial-ready — FSM interruptibility, config schema, UX fixes" +created: 2026-04-13 +updated: 2026-04-13 +status: proven +tags: [session, rotatingMachine, state-machine, docker, e2e] +--- + +# 2026-04-13 — rotatingMachine trial-ready + +## Scope + +Honest review + production-hardening pass on `rotatingMachine`. Fixes landed on top of the 2026-04-07 hardening and are verified against a Docker-hosted Node-RED stack. + +## Findings (before fixes) + +From a live E2E run captured via the Node-RED debug websocket (`/comms`): + +- **Clean startup→operational→shutdown→idle path** works to spec: 3 s starting + 2 s warmup + 3 s stopping + 2 s cooldown, matching config exactly. +- **Tick cadence:** 1000 ms (min 1000, max 1005, avg 1002.5). +- **Predictions** gate correctly on pressure injection; at 900 mbar Δp the hidrostal-H05K-S03R curve yields a monotonic flow/power response. +- **State machine FSM** *rejects* `stopping`/`coolingdown`/`idle` transitions while the machine is in `accelerating`/`decelerating`, leaving a shutdown command silently dropped. Log symptom: `Invalid transition from accelerating to stopping. Transition not executed.` +- **Sequence `emergencyStop` not defined** warn appears when a parent orchestrator with the capital-S casing (e.g. `machineGroupControl` config) forwards the sequence name. +- **Config validator strips** `functionality.distance` and top-level `output` that `buildConfig` adds; every deploy prints removal warnings. +- Cosmetic: typo "acurate" in single-side pressure warn; editor lacks unit hints for `speed` / `startup` / etc. + +## Fixes + +### 1. Interruptible movement (`generalFunctions/src/state/state.js`) + +`moveTo`'s `catch` block now detects `Movement aborted` / `Transition aborted` errors and transitions the FSM back to `operational`, unblocking subsequent sequence transitions. A new `movementAborted` event is emitted for observability. + +### 2. Auto-abort on shutdown/emergency-stop (`rotatingMachine/src/specificClass.js`) + +`executeSequence` now: + +- Normalizes the sequence name to lowercase (defensive against parent callers using mixed case). +- When `shutdown` or `emergencystop` is requested from `accelerating`/`decelerating`, calls `state.abortCurrentMovement(...)` and waits up to 2 s for the FSM to return to `operational` via the new `_waitForOperational(timeoutMs)` helper that listens on the state emitter. + +### 3. Config schema sync (`generalFunctions/src/configs/rotatingMachine.json`) + +Added to the schema: + +- `functionality.distance`, `.distanceUnit`, `.distanceDescription` (produced by the HTML editor). +- Top-level `output.process` / `output.dbase` (produced by `buildConfig`). + +Also reverted an overly broad `buildConfig` addition to only emit `distance` (not `distanceUnit`/`distanceDescription`) so other nodes aren't forced to add these to their schemas. + +### 4. UX polish + +- Fixed typo "acurate" → "accurate" in the single-side pressure warning, plus made the message actionable. +- Added unit hints to Reaction Speed / Startup / Warmup / Shutdown / Cooldown fields in the editor. +- Expanded the Node-RED help panel with a topic reference, state diagram, prediction rules, and port documentation. + +## Tests added + +`test/integration/interruptible-movement.integration.test.js` — three regression tests for the FSM fix: + +- `shutdown during accelerating aborts the move and reaches idle` +- `emergency stop during accelerating reaches off` +- `executeSequence accepts mixed-case sequence names` + +`test/integration/curve-prediction.integration.test.js` — 12 parametrized tests across both shipped pump curves (`hidrostal-H05K-S03R` and `hidrostal-C5-D03R-SHN1`): + +- Curve loader returns nq + np with matching pressure slices. +- Predicted flow and power at mid-pressure / mid-ctrl are finite and inside the curve envelope. +- Flow is monotonically non-decreasing across a ctrl sweep at fixed pressure. +- Flow decreases (or stays level) when pressure rises at fixed ctrl — centrifugal-pump physics. +- CoG / NCog are computed, finite, and inside [0, 100] controller units. +- Reverse predictor (flow → ctrl via reversed nq) round-trips within 10 % of the known controller position. + +`test/e2e/curve-prediction-benchmark.py` + `test/e2e/README.md` — live Dockerized Node-RED benchmark that deploys one rotatingMachine per curve and records a (pressure × ctrl) sweep. + +Full unit suite: **91/91 passing** (was 76/76 on the morning review). + +## E2E verification (Dockerized Node-RED) + +Via `/tmp/rm_e2e_verify.py` — deploys the example flow to `docker compose`-hosted Node-RED, drives it via `POST /inject/:id`, captures port-output via `ws://localhost:1880/comms`. + +| Scenario | Observed state sequence | Pass? | +|---|---|---| +| Shutdown fired while `accelerating` | starting → warmingup → operational → accelerating → decelerating → stopping → coolingdown → **idle** | ✅ | +| Emergency stop fired while `accelerating` | starting → warmingup → operational → accelerating → **off** | ✅ | +| Clean startup → shutdown (regression) | starting → warmingup → operational → stopping → coolingdown → idle | ✅ | + +Container log scan over a 3-minute window: + +- `Unknown key` warns: 0 (was 6+ per deploy) +- `acurate` typo: 0 (was 2) +- `Invalid transition from accelerating/decelerating to ...` errors: 0 (was 3+) +- `Sequence '...' not defined`: 0 (was 1) + +### Dual-curve prediction sweep + +Via `nodes/rotatingMachine/test/e2e/curve-prediction-benchmark.py`. Deploys two live rotatingMachines, one per pump curve, and runs a (pressure × ctrl) sweep per pump. Each pump is tested only inside its own curve envelope. + +| Pump | Pressures swept (mbar) | Ctrl setpoints (%) | Samples in envelope | Flow monotonic | Flow observed (m³/h) | Power observed (kW) | +|---|---|---|---|---|---|---| +| hidrostal-H05K-S03R | 700 / 2300 / 3900 | 20 / 40 / 60 / 80 | 12/12 ✅ | ✅ | 10.3 – 208.3 | 12.3 – 50.3 | +| hidrostal-C5-D03R-SHN1 | 400 / 1700 / 2900 | 20 / 40 / 60 / 80 | 12/12 ✅ | ✅ | 8.7 – 45.6 | 0.7 – 13.0 | + +Inverse-pressure monotonicity (centrifugal-pump physics) also verified: for both pumps, flow at the highest pressure slice is strictly lower than flow at the lowest pressure slice for the same ctrl. + +**Known limitation** captured in the memory file: extrapolating pressure *below* the curve's minimum slice produces nonsensical flow values (e.g. H05K at 400 mbar ctrl=20% predicts ~30 000 m³/h vs envelope max 227 m³/h). Upstream `measurement` nodes are expected to clamp sensors to realistic ranges; rotatingMachine itself does not. + +Separately, the C5 curve still exhibits the previously-documented power non-monotonicity at p=1700 mbar (sparse-data spline artefact noted in the 2026-04-07 session); this is compensated by the group-optimization marginal-cost refinement loop. + +## Files changed + +``` +nodes/generalFunctions/src/state/state.js # abort recovery +nodes/generalFunctions/src/configs/index.js # buildConfig trim +nodes/generalFunctions/src/configs/rotatingMachine.json # schema sync +nodes/rotatingMachine/src/specificClass.js # exec + typo +nodes/rotatingMachine/rotatingMachine.html # UX hints + help +nodes/rotatingMachine/test/integration/interruptible-movement.integration.test.js # +3 tests (FSM) +nodes/rotatingMachine/test/integration/curve-prediction.integration.test.js # +12 tests (dual curve) +nodes/rotatingMachine/test/e2e/curve-prediction-benchmark.py # new E2E benchmark +nodes/rotatingMachine/test/e2e/README.md # benchmark docs +nodes/rotatingMachine/README.md # rewrite +``` + +## Production readiness + +Status: **trial-ready**. The caveats flagged in the 2026-04-13 memory file (`node_rotatingMachine.md`) are resolved. Remaining items are in the wishlist (interruptible curve validation feedback, domain review of ctrl≈0% + backpressure flow prediction, opt-in full-snapshot port-0 mode, per-machine `/health` endpoint). + +## Verification command + +```bash +cd /mnt/d/gitea/EVOLV +docker compose up -d nodered influxdb +cd nodes/rotatingMachine && npm test +python3 /tmp/rm_e2e_verify.py # end-to-end smoke +```