Dashboard was a single page — 30+ widgets + tiny charts competing for
space. Trends were invisible or very small (width/height both "0"
meant "inherit from group" which gave near-zero chart area).
Split into 3 dashboard pages:
1. Control — Process Demand, Station Controls, MGC/Basin status,
per-pump panels (unchanged, just moved off trend groups)
2. Trends — 10 min — rolling 10-minute flow + power charts with
width=12 (full group), height=8 (tall charts), 300 max points
3. Trends — 1 hour — same layout with 60-minute window, 1800 points
All 3 pages auto-nav via the FlowFuse sidebar. Same data feed: the
per-pump trend_split function now wires to 4 charts (2 outputs × 2
pages) instead of 2.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pumping Station — 3 Pumps with Dashboard
A complete end-to-end EVOLV stack: a wet-well basin model, a machineGroupControl orchestrating three rotatingMachine pumps (each with upstream/downstream pressure measurements), process-demand input from either a dashboard slider or an auto random generator, individual + auto control modes, and a FlowFuse dashboard with status, gauges, and trend charts.
This is the canonical "make sure everything works together" demo for the platform. Use it after any cross-node refactor to confirm the architecture still hangs together end-to-end.
Quick start
cd /mnt/d/gitea/EVOLV
docker compose up -d
# Wait for http://localhost:1880/nodes to return 200, then:
curl -s -X POST http://localhost:1880/flows \
-H "Content-Type: application/json" \
-H "Node-RED-Deployment-Type: full" \
--data-binary @examples/pumpingstation-3pumps-dashboard/flow.json
Or open Node-RED at http://localhost:1880, Import → drop the flow.json, click Deploy.
Then open the dashboard:
Tabs
The flow is split across four tabs by concern:
| Tab | Lives here | Why |
|---|---|---|
| 🏭 Process Plant | EVOLV nodes (3 pumps + MGC + PS + 6 measurements) and per-node output formatters | The "real plant" layer. Lift this tab into production unchanged. |
| 📊 Dashboard UI | All ui-* widgets, button/setpoint wrappers, trend-split functions |
Display + operator inputs only. No business logic. |
| 🎛️ Demo Drivers | Random demand generator, random-toggle state | Demo-only stimulus. In production, delete this tab and feed cmd:demand from your real demand source. |
| ⚙️ Setup & Init | One-shot once: true injects (MGC scaling/mode, pumps mode, auto-startup, random-on) |
Runs at deploy time only. Disable for production runtimes. |
Cross-tab wiring uses named link-out / link-in pairs, never direct cross-tab wires. The channel names form the contract:
| Channel | Direction | What it carries |
|---|---|---|
cmd:demand |
UI / drivers → process | numeric demand in m³/h |
cmd:randomToggle |
UI → drivers | 'on' / 'off' |
cmd:mode |
UI / setup → process | 'auto' / 'virtualControl' setMode broadcast |
cmd:station-startup / cmd:station-shutdown / cmd:station-estop |
UI / setup → process | station-wide command, fanned to all 3 pumps |
cmd:setpoint-A / -B / -C |
UI → process | per-pump setpoint slider value |
cmd:pump-A-seq / -B-seq / -C-seq |
UI → process | per-pump start/stop |
evt:pump-A / -B / -C |
process → UI | formatted per-pump status |
evt:mgc |
process → UI | MGC totals (flow / power / efficiency) |
evt:ps |
process → UI | basin state + level + volume + flows |
setup:to-mgc |
setup → process | MGC scaling/mode init |
See .claude/rules/node-red-flow-layout.md for the full layout rule set this demo follows.
What the flow contains
| Layer | Node(s) | Role |
|---|---|---|
| Top | pumpingStation "Pumping Station" |
Wet-well basin model. Tracks inflow (q_in), outflow (from machine-group child predictions), basin level/volume. PS is in manual control mode for the demo so it observes without taking control. |
| Mid | machineGroupControl "MGC — Pump Group" |
Distributes Qd flow demand across the 3 pumps via optimalcontrol (BEP-driven). Scaling: absolute (Qd is in m³/h directly). |
| Low | rotatingMachine × 3 — Pump A / B / C |
Hidrostal H05K-S03R curve. auto mode by default so MGC's parent commands are accepted. Manual setpoint slider overrides per-pump when each is in virtualControl. |
| Sensors | measurement × 6 |
Per pump: upstream + downstream pressure (mbar). Simulator mode — each ticks a random-walk value continuously. Registered as children of their pump. |
| Demand | inject demand_rand_tick + function demand_rand_fn + ui-slider |
Random generator (3 s tick, [40, 240] m³/h) AND a manual slider. Both feed a router that fans out to PS (q_in in m³/s) and MGC (Qd in m³/h). |
| Glue | setMode fanouts + station-wide buttons |
Mode toggle broadcasts setMode to all 3 pumps. Station-wide Start / Stop / Emergency-Stop buttons fan out to all 3. |
| Dashboard | FlowFuse ui-page + 6 groups |
Process Demand · Pumping Station · Pump A · Pump B · Pump C · Trends. |
Dashboard map
The page (/dashboard/pumping-station-demo) is laid out top-to-bottom:
- Process Demand
- Slider 0–300 m³/h (
manualDemandtopic) - Random demand toggle (auto cycles every 3 s)
- Live "current demand" text
- Slider 0–300 m³/h (
- Pumping Station
- Auto/Manual mode toggle (drives all pumps'
setModesimultaneously) - Station-wide buttons: Start all · Stop all · Emergency stop
- Basin state, level (m), volume (m³), inflow / pumped-out flow (m³/h)
- Auto/Manual mode toggle (drives all pumps'
- Pump A / B / C (one group each)
- Setpoint slider 0–100 % (only effective when that pump is in
virtualControl) - Per-pump Startup + Shutdown buttons
- Live state, mode, controller %, flow, power, upstream/downstream pressure
- Setpoint slider 0–100 % (only effective when that pump is in
- Trends
- Flow per pump chart (m³/h)
- Power per pump chart (kW)
Control model
- AUTO — the default.
setMode auto→ MGC'soptimalcontroldecides which pumps run and at what flow. Operator drives only the Process Demand slider (or leaves the random generator on); the per-pump setpoint sliders are ignored. - MANUAL — flip the Auto/Manual switch. All 3 pumps go to
virtualControl. MGC commands are now ignored. Per-pump setpoint sliders / Start / Stop are the only inputs that affect the pumps.
The Emergency Stop button always works regardless of mode and uses the new interruptible-movement path so it stops a pump mid-ramp.
Notable design choices
- PS is in
manualcontrol mode (controlMode: "manual"). The defaultlevelbasedmode would auto-shut all pumps as soon as basin level dips belowstopLevel(1 m default), which masks the demo. Manual = observation only. - PS safety guards (dry-run / overfill) disabled. With no real inflow the basin will frequently look "empty" — that's expected for a demo, not a fault. In production you'd configure a real
q_insource and leave safeties on. - MGC scaling =
absolute, mode =optimalcontrol. Set via inject at deploy. Demand in m³/h, BEP-driven distribution. - demand_router gates Qd ≤ 0. A demand of 0 would shut every running pump (via MGC.turnOffAllMachines). Use the explicit Stop All button to actually take pumps down.
- Auto-startup on deploy. All three pumps fire
execSequence startup4 s after deploy so the dashboard shows activity immediately. - Auto-enable random demand 5 s after deploy so the trends fill in without operator action.
- Verbose logging is OFF. All EVOLV nodes are at
warn. Crank the per-nodelogLeveltoinfoordebugif you're diagnosing a flow.
Things to try
- Drag the Process Demand slider with random off — watch MGC distribute that target across pumps and the basin start filling/draining accordingly.
- Flip to Manual mode and use the per-pump setpoint sliders — note that MGC stops driving them.
- Hit Emergency Stop while a pump is ramping — confirms the interruptible-movement fix shipped in
rotatingMachinev1.0.3. - Watch the Trends chart over a few minutes — flow distribution shifts as MGC re-balances around the BEP.
Verification (last green run, 2026-04-13)
Deployed via POST /flows to a Dockerized Node-RED, observed for ~15 s after auto-startup:
- All 3 measurement nodes per pump tick (6 total): pressure values stream every second.
- Each pump reaches
operational~5 s after the auto-startup inject (3 s starting + 1 s warmup + 1 s for setpoint=0 settle). - MGC reports
3 machine(s) connectedwith modeoptimalcontrol. - Pumping Station shows non-zero basin volume + tracks net flow direction (⬆ / ⬇ / ⏸).
- Random demand cycles between ~40 and ~240 m³/h every 3 s.
- Per-pump status text + trend chart update on every tick.
Regenerating flow.json
flow.json is generated from build_flow.py. Edit the Python (cleaner diff) and regenerate:
cd examples/pumpingstation-3pumps-dashboard
python3 build_flow.py > flow.json
The build_flow.py is the source of truth — keep it in sync if you tweak the demo.
Wishlist (not in this demo, build separately)
- Pump failure + MGC re-routing — kill pump 2 mid-run, watch MGC redistribute. Would demonstrate fault-tolerance.
- Energy-optimal vs equal-flow control — same demand profile run through
optimalcontrolandprioritycontrolmodes side-by-side, energy comparison chart. - Schedule-driven demand — diurnal flow pattern (low at night, peak at 7 am), MGC auto-tuning over 24 simulated hours.
- PS with real
q_insource + safeties on — show the basin auto-shut behaviour as a feature, not a bug. - Real flow sensor per pump (vs. relying on rotatingMachine's predicted flow) — would let the demo also show measurement-vs-prediction drift indicators.
- Reactor or settler downstream — close the loop on a real wastewater scenario.
See the parent examples/README.md for the full follow-up catalogue.