feat(mgc): rendezvous planner — same-time landing across all modes
Routes every dispatch through a tick-aware planner so all pumps reach
their setpoint at the same wall-clock instant t* = max(eta_i),
regardless of control strategy or per-pump reaction speed.
Architecture (src/movement/):
- machineProfile.js – pure snapshot of a registered child (state,
position, velocityPctPerS, ladder timings,
flowAt / positionForFlow). Reads timings from
child.state.config.time (the actual storage
location — previous fallback paths silently
produced 0 s, collapsing every eta to ramp-only).
- moveTrajectory.js – seconds-to-target per machine; handles
idle / starting / warmingup / operational / cooling.
- movementScheduler.js – t* = max eta over ALL non-noop moves. Every
command is delayed so its move finishes at t*.
Startup execsequence fires at 0; its flowmovement
is gated by max(ladderS, t* − rampS) so a fast
pump waits before ramping rather than landing
early. useRendezvous=false collapses to all
fireAtTickN=0 (legacy fire-and-forget).
- movementExecutor.js – wall-clock virtual cursor: each tick fires
every command whose fireAtTickN ≤ floor(elapsed/tickS).
tick() no longer awaits pending fireCommand
promises — the synchronous prologue of
handleInput claims the latest-wins gate, which
is what race-favouring relies on.
Shared dispatch path (src/specificClass.js):
- _dispatchFlowDistribution(distribution) — extracted from
_optimalControl. Builds profiles, calls movementScheduler.plan,
replans the executor, ticks once. Reads
config.planner.useRendezvous (default true).
- _optimalControl computes its bestCombination and hands off.
- equalFlowControl (priorityControl mode) computes its
flowDistribution and hands off via ctx.mgc._dispatchFlowDistribution.
Same-time landing now applies in BOTH modes.
Editor toggle (mgc.html + src/nodeClass.js):
- New "Same-time landing" checkbox under Control Strategy.
- nodeClass.buildDomainConfig bridges uiConfig.useRendezvous →
config.planner.useRendezvous. Default ON.
Tests:
- New: planner-convergence.integration.test.js (real-time end-to-end
diagnostic — drives a 3-pump mixed-state dispatch and asserts both
convergence to the demand setpoint AND same-time landing within
one tick).
- New: planner-rendezvous.integration.test.js (schedule-shape
assertions against real pump objects).
- New: movementScheduler.basic.test.js — includes a mixed-speed
multi-startup case proving the fast pumps wait so all three land
together (the regression that prompted this work).
- New: movementExecutor.basic.test.js + moveTrajectory.basic.test.js.
- Updated executor contract test: tick() must NOT await pending fires.
Commands + wiki:
- handlers.js: source/mode allow-list gate moved into a shared _gate()
helper; every command now checks isValidActionForMode +
isValidSourceForMode before dispatching. Status-level commands
(set.mode, set.scaling) are allowed in every mode.
- commands.basic.test.js: coverage for the new gate behaviour.
- wiki regen: Home.md visual-first rewrite + 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:
155
wiki/Reference-Examples.md
Normal file
155
wiki/Reference-Examples.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# Reference — Examples
|
||||
|
||||

|
||||
|
||||
> [!NOTE]
|
||||
> Every example flow shipped under `nodes/machineGroupControl/examples/`, plus how to load them, what they show, and the debug recipes that go with them. Live source: `nodes/machineGroupControl/examples/`.
|
||||
|
||||
---
|
||||
|
||||
## Shipped examples
|
||||
|
||||
| File | Tier | What it shows |
|
||||
|:---|:---:|:---|
|
||||
| `examples/01-Basic.json` | 1 | One MGC + three `rotatingMachine` pumps driven by inject buttons. A Setup group once-fires `virtualControl` + `cmd.startup` on all three pumps; mode / demand are then driven by buttons. |
|
||||
| `examples/02-Dashboard.json` | 2 | Same command surface driven by a FlowFuse Dashboard 2.0 page — mode buttons, demand slider, live status rows (mode / total flow / total power / capacity / active machines / BEP %), trend charts, and a raw-output table. |
|
||||
|
||||
MGC is not a standalone node — it needs at least one `rotatingMachine` child to dispatch to. Both flows ship three child pumps.
|
||||
|
||||
---
|
||||
|
||||
## Loading a flow
|
||||
|
||||
### Via the editor
|
||||
|
||||
1. Open the Node-RED editor at `http://localhost:1880`.
|
||||
2. Menu → Import.
|
||||
3. Drag-and-drop the JSON file, or paste its contents.
|
||||
4. Click Deploy.
|
||||
|
||||
### Via the Admin API
|
||||
|
||||
```bash
|
||||
curl -X POST -H 'Content-Type: application/json' \
|
||||
--data @nodes/machineGroupControl/examples/01-Basic.json \
|
||||
http://localhost:1880/flows
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 01 — Basic standalone
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Screenshot needed.** Capture of the basic flow in the editor. Save as `wiki/_partial-screenshots/machineGroupControl/01-basic-flow.png`. Replace this callout with the image link.
|
||||
|
||||
### Nodes on the tab
|
||||
|
||||
| Type | Purpose |
|
||||
|:---|:---|
|
||||
| `comment` | Tab header / instructions / driver-group labels |
|
||||
| `inject` | Setup auto-injects (virtualControl + cmd.startup per pump), mode buttons, demand-by-percent buttons, demand-by-absolute-unit buttons, stop-all button |
|
||||
| `machineGroupControl` | The unit under test |
|
||||
| `rotatingMachine` × 3 | Children A / B / C (each with its own simulated pressure pair) |
|
||||
| `debug` | Port 0 (process), Port 1 (telemetry), Port 2 (registration) per node |
|
||||
|
||||
### What to do after deploy
|
||||
|
||||
1. Wait ~1.5 s. The Setup group auto-fires `virtualControl` + `cmd.startup` on all three pumps.
|
||||
2. Click `set.demand = 50` (bare number = percent). MGC selects the best combination via BEP-Gravitation, plans a rendezvous, and dispatches `flowmovement` to the selected pumps.
|
||||
3. Click `set.demand = 100`. The optimizer probably engages a third pump; the planner schedules its `execsequence(startup)` at tick 0 and delays the running pumps' down-moves so they all hit their new targets together at `t*`.
|
||||
4. Click `set.mode = priorityControl`. Subsequent demands route through `equalFlowControl` — equal-flow per active pump in priority order. (Planner is bypassed in this mode — see [Limitations](Reference-Limitations).)
|
||||
5. Click `set.demand = {value: 80, unit: 'm3/h'}` (or use the absolute-unit button). Same path, but the percent-mapping step is skipped — the value lands on the gate as canonical m³/s directly.
|
||||
6. Click `set.demand = -1`. `turnOffAllMachines` runs: cancels any parked demand, sends `execsequence: 'shutdown'` to every active pump.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **GIF needed.** Demo of steps 1–6 with the live status panel. Save as `wiki/_partial-gifs/machineGroupControl/01-basic-demo.gif`, target ≤ 1 MB after `gifsicle -O3 --lossy=80`.
|
||||
|
||||
---
|
||||
|
||||
## Example 02 — Dashboard
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Screenshots needed.** Two captures from `02-Dashboard.json`:
|
||||
> 1. The editor tab (left controls column + MGC + 3 pumps + dashboard widget cluster on the right).
|
||||
> 2. The rendered dashboard at `http://localhost:1880/dashboard/mgc-basic`.
|
||||
>
|
||||
> Save as `wiki/_partial-screenshots/machineGroupControl/02-dashboard-editor.png` and `03-dashboard-rendered.png`. Replace this callout with both image links.
|
||||
|
||||
### What it adds vs Example 01
|
||||
|
||||
| Addition | Why |
|
||||
|:---|:---|
|
||||
| FlowFuse `ui-base` + `ui-theme` + `ui-page` setup | One dashboard page hosting four widget groups |
|
||||
| `ui-button` cluster (Controls) | Mode buttons, `Initialize pumps`, `Stop all` |
|
||||
| `ui-slider` (Demand) | Drag-to-set demand; passes through the same canonical `set.demand` topic the injects use |
|
||||
| `ui-text` cluster (Status) | Mode / total flow / total power / capacity / active machines / BEP % rows |
|
||||
| `ui-chart` × N (Trends) | Flow, power, BEP trends over time |
|
||||
| `ui-template` (Raw output) | Full key/value table of the latest Port 0 payload |
|
||||
| Fan-out function | Caches last-known values so delta-only Port 0 updates never blank a status row, and forwards numeric values to charts |
|
||||
|
||||
The dashboard buttons fire the **same canonical `msg.topic`** as the inject nodes in Example 01 — there is no separate dashboard command surface to learn.
|
||||
|
||||
Required: `@flowfuse/node-red-dashboard` (Dashboard 2.0) installed in the Node-RED instance.
|
||||
|
||||
### What to do after deploy
|
||||
|
||||
1. Open `http://localhost:1880/dashboard/mgc-basic`.
|
||||
2. The page auto-initialises the pumps; the `Initialize pumps` button re-runs the setup manually.
|
||||
3. Drag the **Demand** slider. The Status row's `total flow` and `BEP %` react; the trend charts plot the transition.
|
||||
4. Switch modes. The mode row in Status reflects the change immediately.
|
||||
5. Inspect the **Raw output** table for the full Port-0 surface — `headerDiffPa`, `flowCapacityMax`, `machineCountActive`, `relDistFromPeak`, …
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **GIF needed.** Capture clicking through demand 30 % → 80 % → -1 with the trends reacting. 30–45 s is enough.
|
||||
>
|
||||
> Save as `wiki/_partial-gifs/machineGroupControl/02-dashboard-demo.gif`. Replace this callout with the image link.
|
||||
|
||||
---
|
||||
|
||||
## 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 |
|
||||
|:---|:---|:---|
|
||||
| `mode is not a valid mode` warns every dispatch | `mode.current` is `maintenance` (or a typo). Reset to `optimalControl` or `priorityControl`. | `_runDispatch` switch. |
|
||||
| `No valid combination found (empty set)` | Demand outside the dynamic envelope, OR every child filtered out (state in `off / coolingdown / stopping / emergencystop` or `auto`-mode rejects the action). | `validPumpCombinations` + state of each child. |
|
||||
| Group flow stuck at zero after `set.demand` | Pumps never reached an active state — check per-pump startup logs. | Each pump's `state` on its Port 0. |
|
||||
| Pump warmingup, but then drops back to idle when demand keeps changing | Pre-2026-05-15 race condition: shutdown's for-loop barged through after a residue-handler operational transition. The fix is the `sequenceAbortToken` mechanism in rotatingMachine's FSM. Verify the rotatingMachine submodule is at `394a972` or newer. | rotatingMachine `state/sequenceController.js`. |
|
||||
| Header pressure not equalising | Pressure children must register with `asset.type='pressure'` and a matching `positionVsParent`. Pure-numeric pressures with no unit are rejected by MeasurementContainer. | `operatingPoint.equalize`. |
|
||||
| Optimiser picks unexpected combination | Verify `optimization.method` — default is `BEP-Gravitation-Directional`. Per-method scoring lives in `optimizer/`. | `optimizer/{bestCombination, bepGravitation}.js`. |
|
||||
| Status badge shows `scaling=norm` even after a unit-tagged demand | Badge cosmetic only — the `scaling` field is a legacy artifact and currently always reads `norm`. The dispatch path is unit-self-describing. | `io/output.js` `getStatusBadge`. |
|
||||
| Per-pump flow / power trends missing | MGC only emits group aggregates on Port 0. Subscribe to each `rotatingMachine`'s Port 0 if you need per-pump series. | `io/output.js` `getOutput`. |
|
||||
|
||||
> 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, dispatch lifecycle, planner internals |
|
||||
| [Reference — Limitations](Reference-Limitations) | Known issues and open questions |
|
||||
| [EVOLV — Topology Patterns](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topology-Patterns) | Where this node fits in a larger plant |
|
||||
Reference in New Issue
Block a user