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>
8.6 KiB
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
- Open the Node-RED editor at
http://localhost:1880. - Menu → Import.
- Drag-and-drop the JSON file, or paste its contents.
- Click Deploy.
Via the Admin API
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
- Wait ~1.5 s. The Setup group auto-fires
virtualControl+cmd.startupon all three pumps. - Click
set.demand = 50(bare number = percent). MGC selects the best combination via BEP-Gravitation, plans a rendezvous, and dispatchesflowmovementto the selected pumps. - Click
set.demand = 100. The optimizer probably engages a third pump; the planner schedules itsexecsequence(startup)at tick 0 and delays the running pumps' down-moves so they all hit their new targets together att*. - Click
set.mode = priorityControl. Subsequent demands route throughequalFlowControl— equal-flow per active pump in priority order. (Planner is bypassed in this mode — see Limitations.) - 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. - Click
set.demand = -1.turnOffAllMachinesruns: cancels any parked demand, sendsexecsequence: '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 aftergifsicle -O3 --lossy=80.
Example 02 — Dashboard
Important
Screenshots needed. Two captures from
02-Dashboard.json:
- The editor tab (left controls column + MGC + 3 pumps + dashboard widget cluster on the right).
- The rendered dashboard at
http://localhost:1880/dashboard/mgc-basic.Save as
wiki/_partial-screenshots/machineGroupControl/02-dashboard-editor.pngand03-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
- Open
http://localhost:1880/dashboard/mgc-basic. - The page auto-initialises the pumps; the
Initialize pumpsbutton re-runs the setup manually. - Drag the Demand slider. The Status row's
total flowandBEP %react; the trend charts plot the transition. - Switch modes. The mode row in Status reflects the change immediately.
- 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:
# 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.
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 | Intuitive overview |
| Reference — Contracts | Topic + config + child filters |
| Reference — Architecture | Code map, dispatch lifecycle, planner internals |
| Reference — Limitations | Known issues and open questions |
| EVOLV — Topology Patterns | Where this node fits in a larger plant |