feat(state): honor sequenceAbortToken so external aborts cleanly break sequences
Consumer half of the abort-token mechanism added in generalFunctions
state.js. executeSequence captures host.state.sequenceAbortToken at
entry, then re-checks before every state transition and after the
optional ramp-down. If MGC (or any external caller) bumps the token
mid-sequence, the loop bails out cleanly — no more barge-through where
a pre-empted shutdown advances through stopping → coolingdown after a
fresh demand has already engaged the pump.
Without this the MGC rendezvous planner can't reliably re-dispatch a
pump that's mid-shutdown: the new flowmovement claims the gate, but
the old shutdown's for-loop keeps running on microtasks and steps the
FSM into idle/off underneath it.
Also: wiki regen following the same visual-first 14-section template as
the other EVOLV nodes — Reference-{Architecture,Contracts,Examples,
Limitations}.md split with _Sidebar.md index.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
408
wiki/Home.md
408
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 /<br/>pumpingStation]:::unit -->|flowmovement<br/>execsequence| rm[rotatingMachine<br/>Equipment]:::equip
|
||||
m_up[measurement<br/>pressure upstream]:::ctrl -.data.-> rm
|
||||
m_dn[measurement<br/>pressure downstream]:::ctrl -.data.-> rm
|
||||
sim[dashboard-sim<br/>virtual pressure children]:::ctrl -.data.-> rm
|
||||
m_up[measurement<br/>pressure upstream]:::ctrl -.measured.-> rm
|
||||
m_dn[measurement<br/>pressure downstream]:::ctrl -.measured.-> rm
|
||||
sim[dashboard-sim-upstream /<br/>dashboard-sim-downstream<br/>(auto-registered virtual children)]:::ctrl -.measured.-> rm
|
||||
rm -->|child.register| parent
|
||||
rm -.->|flow.predicted.*<br/>power.predicted.atequipment| parent
|
||||
classDef unit fill:#50a8d9,color:#000
|
||||
@@ -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()<br/>static DomainClass, commands"]
|
||||
end
|
||||
subgraph domain["specificClass.js — orchestrator (BaseDomain)"]
|
||||
sc["Machine.configure()<br/>_setupCurves / _setupState /<br/>_setupDrift / _setupPressure /<br/>_setupChildren"]
|
||||
end
|
||||
subgraph concerns["src/ concern modules"]
|
||||
curves["curves/<br/>loadModelCurve + normalize"]
|
||||
prediction["prediction/<br/>buildPredictors + math"]
|
||||
drift["drift/<br/>DriftAssessor + healthRefresh"]
|
||||
pressure["pressure/<br/>init + router + selector + virtual"]
|
||||
state["state/<br/>FSM bindings + sequenceController"]
|
||||
measurement["measurement/<br/>handlers + childRegistrar"]
|
||||
flow["flow/<br/>flowController (handleInput)"]
|
||||
display["display/<br/>workingCurves + CoG"]
|
||||
io["io/<br/>output + status"]
|
||||
commands["commands/<br/>topic registry + handlers"]
|
||||
end
|
||||
nc --> sc
|
||||
sc --> curves
|
||||
sc --> prediction
|
||||
sc --> drift
|
||||
sc --> pressure
|
||||
sc --> state
|
||||
sc --> measurement
|
||||
sc --> flow
|
||||
sc --> display
|
||||
sc --> io
|
||||
nc --> commands
|
||||
```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`.
|
||||
|
||||
<!-- BEGIN AUTOGEN: topic-contract -->
|
||||
---
|
||||
|
||||
| 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
|
||||
|
||||
<!-- END AUTOGEN: topic-contract -->
|
||||
| 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 `<asset.type>.measured.<positionVsParent>` 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<br/>type=pressure<br/>position=upstream"]:::ctrl
|
||||
m_pd["measurement<br/>type=pressure<br/>position=downstream"]:::ctrl
|
||||
m_f["measurement<br/>type=flow"]:::ctrl
|
||||
m_pw["measurement<br/>type=power"]:::ctrl
|
||||
m_t["measurement<br/>type=temperature"]:::ctrl
|
||||
end
|
||||
m_pu -->|pressure.measured.upstream| router[pressureRouter.route]
|
||||
m_pd -->|pressure.measured.downstream| router
|
||||
m_f -->|flow.measured.<pos>| mh[measurementHandlers]
|
||||
m_pw -->|power.measured.atequipment| mh
|
||||
m_t -->|temperature.measured.<pos>| mh
|
||||
router --> upd[updatePosition + drift refresh]
|
||||
mh --> upd
|
||||
classDef ctrl fill:#a9daee,color:#000
|
||||
```
|
||||
---
|
||||
|
||||
| softwareType | filter | wired to | side-effect |
|
||||
|---|---|---|---|
|
||||
| `measurement` | `type=pressure, position=upstream` | `pressureRouter.route('upstream', ...)` | Sets upstream pressure; refresh prediction + drift. |
|
||||
| `measurement` | `type=pressure, position=downstream` | `pressureRouter.route('downstream', ...)` | Sets downstream pressure; refresh prediction + drift. |
|
||||
| `measurement` | `type=flow, position=*` | `measurementHandlers.updateMeasuredFlow` | Stored; drift assessed against predicted. |
|
||||
| `measurement` | `type=power, position=atEquipment` | `measurementHandlers.updateMeasuredPower` | Stored; drift assessed against predicted. |
|
||||
| `measurement` | `type=temperature, position=*` | `measurementHandlers.updateMeasuredTemperature` | Stored; used by power correction if relevant. |
|
||||
## 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.
|
||||
|
||||
<!-- BEGIN AUTOGEN: data-model -->
|
||||
|
||||
| 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` |
|
||||
|
||||
<!-- END AUTOGEN: data-model -->
|
||||
|
||||
**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 `<childId>` 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: **`<type>.<variant>.<position>.<childId>`** — the inverse of MGC's key shape, because rotatingMachine emits per-measurement snapshots. The trailing `<childId>` is the registering child's id (`dashboard-sim-upstream`, `dashboard-sim-downstream`, or `default` for own predictions). Position labels are normalised to lowercase in keys.
|
||||
|
||||
## 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)
|
||||
|
||||
Reference in New Issue
Block a user