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:
znetsixe
2026-05-17 19:44:48 +02:00
parent 394a972d10
commit 5ea0b0bda6
7 changed files with 1034 additions and 304 deletions

169
wiki/Reference-Examples.md Normal file
View File

@@ -0,0 +1,169 @@
# Reference &mdash; Examples
![code-ref](https://img.shields.io/badge/code--ref-394a972-blue)
> [!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 &mdash; 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 &mdash; 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 &rarr; Import &rarr; 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 &mdash; 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` &times; 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` &times; 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 &rarr; starting &rarr; warmingup &rarr; operational`. `runtime` starts accumulating.
4. Click `set.setpoint = 60` (control %). `state` goes `operational &rarr; accelerating &rarr; 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'}` &mdash; same path, but the setpoint is a flow value; the node converts via `predictCtrl` to a control %.
6. Click `cmd.shutdown`. State: `operational &rarr; decelerating &rarr; stopping &rarr; coolingdown &rarr; 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&ndash;6 + the status badge progression. Save as `wiki/_partial-gifs/rotatingMachine/01-basic-demo.gif`, target &le; 1&nbsp;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 &rarr; decelerating &rarr; …`.
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 &rarr; coolingdown &rarr; 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 &mdash; with `delayedMove = 60` sitting unused.
---
## Example 02 &mdash; 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 &mdash; 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 `<source> is not allowed in mode <mode>` or `<action> is not allowed in mode <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 &mdash; 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 &mdash; 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 `<type>.<variant>.<position>.<childId>` (e.g. `flow.predicted.downstream.default`). MGC uses `<position>_<variant>_<type>`. Don't mix them. | `io/output.js`, `MeasurementContainer.getFlattenedOutput`. |
> Never ship `enableLog: 'debug'` in a demo &mdash; fills the container log within seconds and obscures real errors.
---
## Related pages
| Page | Why |
|:---|:---|
| [Home](Home) | Intuitive overview |
| [Reference &mdash; Contracts](Reference-Contracts) | Topic + config + child filters |
| [Reference &mdash; Architecture](Reference-Architecture) | Code map, FSM, prediction + drift pipeline |
| [Reference &mdash; Limitations](Reference-Limitations) | Known issues and open questions |
| [machineGroupControl &mdash; Examples](https://gitea.wbd-rd.nl/RnD/machineGroupControl/wiki/Reference-Examples) | Group-control demo flows |
| [EVOLV &mdash; Topology Patterns](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topology-Patterns) | Where rotatingMachine fits in a larger plant |