Files
machineGroupControl/wiki/Reference-Examples.md
znetsixe 472402c62d 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>
2026-05-17 19:43:55 +02:00

8.6 KiB
Raw Permalink Blame History

Reference — Examples

code-ref

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

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.)
  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 16 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. 3045 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.


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