Pumping-station demo overhaul + cross-node test harness + bumps
Some checks failed
CI / lint-and-test (push) Has been cancelled
Some checks failed
CI / lint-and-test (push) Has been cancelled
Submodule bumps land the deadlock fix (state.js residue unpark + MGC optimalControl dispatch reorder) and pumpingStation stopLevel hysteresis. - Renames examples/pumpingstation-3pumps-dashboard → pumpingstation-complete-example with regenerated flow.json. New dashboard groups, demand-broadcast wiring, S88 placement rule applied, ui-chart trend-split and link-channel naming follow .claude/rules/node-red-flow-layout.md. - New cross-node test harness under test/: end-to-end-pumpingstation drives PS + MGC + 3 pumps + physics simulator end-to-end and verifies the ~5/15 min cycle. - Adds Grafana provisioning dashboards (pumping-station.json) and a helper sync-example.sh script for export/import to live Node-RED. - Docker entrypoint + settings + compose tweaks for the persistent user dir layout used by the demo. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
# EVOLV — End-to-End Example Flows
|
||||
|
||||
> **Working with these examples?** See [`WORKFLOW.md`](WORKFLOW.md) — the canonical guide for editing, switching projects, persistence, and debugging.
|
||||
|
||||
Demo flows that show how multiple EVOLV nodes work together in a realistic wastewater-automation scenario. Each example is self-contained: its folder has a `flow.json` you can import directly into Node-RED plus a `README.md` that walks through the topology, control modes, and dashboard layout.
|
||||
|
||||
These flows complement the per-node example flows under `nodes/<name>/examples/` (which exercise a single node in isolation). Use the per-node flows for smoke tests during development; use the flows here when you want to see how a real plant section behaves end-to-end.
|
||||
@@ -8,25 +10,34 @@ These flows complement the per-node example flows under `nodes/<name>/examples/`
|
||||
|
||||
| Folder | What it shows |
|
||||
|---|---|
|
||||
| [`pumpingstation-3pumps-dashboard/`](pumpingstation-3pumps-dashboard/) | Wet-well basin + machineGroupControl orchestrating 3 pumps (each with up/downstream pressure measurements), individual + auto control, process-demand input via dashboard slider or random generator, full FlowFuse dashboard. |
|
||||
| [`pumpingstation-complete-example/`](pumpingstation-complete-example/) | End-to-end stack: pumpingStation + MGC + 3 pumps + 12 measurement nodes (4 per pump, physics-coupled), operator-driven inflow with scenario buttons (Constant / Sine / Diurnal / Storm), FlowFuse dashboard (realtime + 1h trends), and provisioned Grafana dashboard backed by InfluxDB. |
|
||||
|
||||
## How to import
|
||||
## How it loads
|
||||
|
||||
1. Bring up the EVOLV stack: `docker compose up -d` from the superproject root.
|
||||
Each subfolder here is a **Node-RED project**. The Docker stack has Node-RED's Projects feature enabled and bootstraps each `examples/<name>/` into `/data/projects/<name>/` on first container start.
|
||||
|
||||
To run:
|
||||
|
||||
1. `docker compose up -d` from the EVOLV root.
|
||||
2. Open Node-RED at `http://localhost:1880`.
|
||||
3. Menu → **Import** → drop in the example's `flow.json` (or paste the contents).
|
||||
3. Menu → **Projects** → **Open Project** → pick one.
|
||||
4. Open the FlowFuse dashboard at `http://localhost:1880/dashboard`.
|
||||
|
||||
Each example uses a unique dashboard `path` so they can coexist in the same Node-RED runtime.
|
||||
The default active project is `pumpingstation-complete-example` (override via `DEFAULT_PROJECT` env var on the nodered service). Switching is two clicks; persistence is handled by the `evolv_nodered_data` named volume — `docker compose down && up` doesn't lose the active flow.
|
||||
|
||||
Each example uses a unique dashboard `path` so they can coexist if you load multiple in the same runtime.
|
||||
|
||||
## Adding new examples
|
||||
|
||||
When you create a new end-to-end example:
|
||||
|
||||
1. Make a subfolder under `examples/` named `<scenario>-<focus>`.
|
||||
2. Include `flow.json` (Node-RED export) and `README.md` (topology, control modes, dashboard map, things to try).
|
||||
3. Test it on a fresh Dockerized Node-RED — clean import, no errors, dashboard loads.
|
||||
4. Add a row to the catalogue table above.
|
||||
2. Include at least `flow.json` and `README.md`. A `build_flow.py` (or equivalent generator) is recommended so the JSON stays diff-friendly.
|
||||
3. `docker compose restart nodered` — the entrypoint will bootstrap your new folder as a Node-RED project (synthesizes `package.json`, `git init`, initial commit) under `/data/projects/<name>/`.
|
||||
4. Editor → Projects → Open Project → pick your new one.
|
||||
5. Add a row to the catalogue table above.
|
||||
|
||||
The bootstrap skips folders that already exist in the volume. To force a refresh of an existing project from the repo source (e.g. after editing `build_flow.py`), use `./scripts/sync-example.sh <name>`.
|
||||
|
||||
## Wishlist for future examples
|
||||
|
||||
|
||||
111
examples/WORKFLOW.md
Normal file
111
examples/WORKFLOW.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# EVOLV Examples — Team Workflow
|
||||
|
||||
This file is the canonical guide for working with the example flows that live under `examples/`. Each subfolder is a Node-RED **project**; the Docker stack is set up so switching between them is two clicks in the editor.
|
||||
|
||||
## Stack at a glance
|
||||
|
||||
| Container | What | URL |
|
||||
|---|---|---|
|
||||
| `evolv-nodered` | Node-RED runtime + dashboard | <http://localhost:1880> · dashboard at <http://localhost:1880/dashboard> |
|
||||
| `evolv-influxdb` | Time-series store (port-1 telemetry) | <http://localhost:8086> · `evolv` / `evolv-dev-pw` |
|
||||
| `evolv-grafana` | Provisioned dashboards (anonymous viewer enabled) | <http://localhost:3000> |
|
||||
|
||||
The `evolv_nodered_data` named volume keeps `/data` (flows, projects, sessions) across `docker compose down && up`. The `examples/` directory in this repo is the **source of truth**; the Node-RED Projects feature operates on a copy in the volume.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
cd /path/to/EVOLV
|
||||
docker compose up -d
|
||||
# Node-RED: http://localhost:1880
|
||||
# Dashboard: http://localhost:1880/dashboard
|
||||
# Grafana: http://localhost:3000 (anonymous viewer)
|
||||
```
|
||||
|
||||
The first time you start it, the entrypoint copies every `examples/<name>/` into `/data/projects/<name>/` and `git init`s each. Subsequent starts skip folders that already exist in the volume.
|
||||
|
||||
## Switching examples
|
||||
|
||||
Open the editor → **menu → Projects → Open Project** → pick another project. The editor reloads the chosen flow.
|
||||
|
||||
The default active project on first boot is `pumpingstation-complete-example`. To change the default for fresh volumes, set `DEFAULT_PROJECT=<name>` on the `nodered` service in `docker-compose.yml`.
|
||||
|
||||
## Editing a flow
|
||||
|
||||
You have two paths. They serve different purposes — pick based on what you're doing.
|
||||
|
||||
### Path A — edit `build_flow.py` (canonical, recommended)
|
||||
|
||||
```bash
|
||||
# 1. Edit the Python generator
|
||||
vim examples/<name>/build_flow.py
|
||||
|
||||
# 2. Regenerate flow.json
|
||||
python3 examples/<name>/build_flow.py > examples/<name>/flow.json
|
||||
|
||||
# 3. Push to the runtime
|
||||
./scripts/sync-example.sh <name>
|
||||
```
|
||||
|
||||
The Python is the **source of truth**. It's diff-friendly and the right place for any change you intend to commit.
|
||||
|
||||
### Path B — edit in the Node-RED editor (experimentation)
|
||||
|
||||
```
|
||||
Open editor → Make changes → Deploy
|
||||
```
|
||||
|
||||
Edits go into the volume (`/data/projects/<name>/flow.json`). They survive `docker compose down && up` but are **not in the EVOLV git repo**. To incorporate them back:
|
||||
|
||||
```bash
|
||||
docker cp evolv-nodered:/data/projects/<name>/flow.json examples/<name>/flow.json
|
||||
```
|
||||
|
||||
Then commit `examples/<name>/flow.json` (and reverse-engineer the change into `build_flow.py` if you want it diff-friendly going forward).
|
||||
|
||||
## Adding a new example
|
||||
|
||||
```bash
|
||||
mkdir examples/<scenario>-<focus>
|
||||
# Build a flow.json (recommended: a build_flow.py that generates it)
|
||||
vim examples/<scenario>-<focus>/{build_flow.py,README.md,flow.json}
|
||||
|
||||
# Restart Node-RED so the entrypoint bootstraps the new project
|
||||
docker compose restart nodered
|
||||
```
|
||||
|
||||
The entrypoint synthesizes `package.json`, runs `git init`, and makes an initial commit so Node-RED recognises it as a project. Bootstrap is idempotent — if a `/data/projects/<name>/` already exists, it's left alone.
|
||||
|
||||
After restart, **Projects → Open Project** in the editor will list the new entry.
|
||||
|
||||
## Resetting state
|
||||
|
||||
| Goal | Command |
|
||||
|---|---|
|
||||
| Push the repo's `flow.json` into the runtime, reload | `./scripts/sync-example.sh <name>` |
|
||||
| Wipe one project's volume copy and re-bootstrap | `docker exec evolv-nodered rm -rf /data/projects/<name>` then `docker compose restart nodered` |
|
||||
| Wipe **everything** in the volume (flows, sessions, all projects, but NOT InfluxDB/Grafana) | `docker compose down && docker volume rm evolv_nodered_data && docker compose up -d` |
|
||||
| Wipe everything including telemetry | `docker compose down -v && docker compose up -d` |
|
||||
|
||||
## Debugging
|
||||
|
||||
| Symptom | Where to look |
|
||||
|---|---|
|
||||
| Flow not loading after deploy | `docker logs evolv-nodered` for crash backtraces |
|
||||
| InfluxDB empty / not receiving | Telemetry tab in editor → status of the `Count writes` node. Should show `N POSTs · M lines (0 err)`. |
|
||||
| Dashboard widget shows `n/a` | Check the Process Plant tab → output formatter function for that node — `c.<key>` keys the dispatcher reads from |
|
||||
| Grafana dashboard panels empty | Open InfluxDB UI (<http://localhost:8086>) → Data Explorer → confirm the field name the panel queries actually exists. Field names are flat dotted keys like `level.predicted.atequipment.default`. |
|
||||
| `interpolation configuration: New f =... is constrained` warnings | The pump curve f-axis is out-of-range. f = downstream − upstream pressure differential, in Pa, must be inside the curve's range (e.g. 70 000 – 390 000 Pa for `hidrostal-H05K-S03R`). Check the per-pump physics feeder formula. |
|
||||
| High CPU in Node-RED | Per-tick HTTP fan-out to InfluxDB; the pumpingstation example uses a 500 ms batch in the Telemetry tab. If CPU is still high, lower `tickIntervalMs` in the EVOLV node configs (currently 1000). |
|
||||
|
||||
## File map per example
|
||||
|
||||
```
|
||||
examples/<name>/
|
||||
├── build_flow.py ← canonical source of flow.json (Python generator)
|
||||
├── flow.json ← regenerated artefact, also tracked in Git
|
||||
├── README.md ← topology, control modes, dashboard map, things to try
|
||||
└── package.json ← (synthesized in volume by entrypoint, not in repo)
|
||||
```
|
||||
|
||||
The repo tracks `build_flow.py`, `flow.json`, and `README.md`. The `package.json` and `.git/` directory of the project live only in the named volume — they're created by the entrypoint on first bootstrap and don't leak back into the EVOLV Git history.
|
||||
@@ -1,140 +0,0 @@
|
||||
# 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
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
- <http://localhost:1880/dashboard/pumping-station-demo>
|
||||
|
||||
## 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:
|
||||
|
||||
1. **Process Demand**
|
||||
- Slider 0–300 m³/h (`manualDemand` topic)
|
||||
- Random demand toggle (auto cycles every 3 s)
|
||||
- Live "current demand" text
|
||||
2. **Pumping Station**
|
||||
- Auto/Manual mode toggle (drives all pumps' `setMode` simultaneously)
|
||||
- Station-wide buttons: Start all · Stop all · Emergency stop
|
||||
- Basin state, level (m), volume (m³), inflow / pumped-out flow (m³/h)
|
||||
3. **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
|
||||
4. **Trends**
|
||||
- Flow per pump chart (m³/h)
|
||||
- Power per pump chart (kW)
|
||||
|
||||
## Control model
|
||||
|
||||
- **AUTO** — the default. `setMode auto` → MGC's `optimalcontrol` decides 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 `manual` control mode** (`controlMode: "manual"`). The default `levelbased` mode would auto-shut all pumps as soon as basin level dips below `minLevel` (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_in` source 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 startup` 4 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-node `logLevel` to `info` or `debug` if 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 `rotatingMachine` v1.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) connected` with mode `optimalcontrol`.
|
||||
- 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:
|
||||
|
||||
```bash
|
||||
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 `optimalcontrol` and `prioritycontrol` modes 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_in` source + 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.
|
||||
File diff suppressed because it is too large
Load Diff
195
examples/pumpingstation-complete-example/README.md
Normal file
195
examples/pumpingstation-complete-example/README.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# Pumping Station — Complete Example
|
||||
|
||||
End-to-end EVOLV stack: 1 pumpingStation + 1 machineGroupControl + 3 rotatingMachine pumps + 12 measurement nodes (4 per pump), wired through Node-RED to InfluxDB and Grafana.
|
||||
|
||||
This is the canonical "everything works together" demo. After any cross-node refactor, run this and verify the Node-RED dashboard, the InfluxDB writes, and the Grafana dashboard all populate.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
cd /home/znetsixe/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-complete-example/flow.json
|
||||
```
|
||||
|
||||
Then open:
|
||||
|
||||
- Node-RED dashboard (realtime + 1h trends): <http://localhost:1880/dashboard>
|
||||
- Grafana dashboard (realtime gauges + historic graphs): <http://localhost:3000> (anonymous viewer is on; the dashboard is `EVOLV / Pumping Station (complete)`)
|
||||
- InfluxDB UI: <http://localhost:8086> (user `evolv` / password `evolv-dev-pw`)
|
||||
|
||||
## What the flow contains
|
||||
|
||||
| Layer | Node(s) | Role |
|
||||
|---|---|---|
|
||||
| Process Cell | `pumpingStation` "Pumping Station" | Wet-well basin model. Levelbased control: drives MGC by basin level. Inflow comes from the Drivers tab; outflow is computed from the pumps. |
|
||||
| Unit | `machineGroupControl` "MGC — Pump Group" | Distributes flow across the 3 pumps via `optimalcontrol`. |
|
||||
| Equipment | `rotatingMachine` × 3 — Pump A / B / C | Hidrostal H05K-S03R curve. Auto by default; manual setpoint slider per pump when in `virtualControl`. |
|
||||
| Control Modules | `measurement` × 12 (4 per pump) | Upstream pressure, downstream pressure, flow, power. Each pump's 4 sensors are driven by a per-pump physics function — values are physically coupled to plant state, not random. |
|
||||
| Telemetry | shared `evt:tlm` link channel → http POST → InfluxDB | Every EVOLV node's port-1 payload is converted to v2 line protocol and POSTed to `telemetry` bucket. |
|
||||
|
||||
## Tabs
|
||||
|
||||
The flow is split across 5 tabs, by **concern**:
|
||||
|
||||
| Tab | Lives here | Why |
|
||||
|---|---|---|
|
||||
| 🏭 **Process Plant** | EVOLV nodes (PS, MGC, 3 pumps, 12 sensors) + per-node output formatters + per-pump physics feeders | The deployable plant model. |
|
||||
| 📊 **Dashboard UI** | All `ui-*` widgets, button/setpoint wrappers, dispatch functions | Display + operator inputs. No business logic. |
|
||||
| 🎛️ **Demo Drivers** | Inflow generator (Constant / Sine / Diurnal / Storm) + 1Hz tick | Inflow is operator-driven via slider + scenario buttons. Outflow is implicit (the pumps drain the basin). |
|
||||
| ⚙️ **Setup & Init** | One-shot `once: true` injects (MGC scaling/mode, pumps mode, initial inflow scenario) | Runs at deploy time only. |
|
||||
| 📈 **Telemetry** | link-in `evt:tlm` → line-protocol function → http POST | InfluxDB writer. |
|
||||
|
||||
Cross-tab wiring uses **named link-out / link-in pairs**, never direct cross-tab wires.
|
||||
|
||||
### Channel contract
|
||||
|
||||
| Channel | Direction | What it carries |
|
||||
|---|---|---|
|
||||
| `cmd:inflow-baseline` | UI → Drivers | numeric m³/h baseline |
|
||||
| `cmd:inflow-scenario` | UI → Drivers | `'constant' \| 'sine' \| 'diurnal' \| 'storm'` |
|
||||
| `cmd:q_in` | Drivers → process | computed inflow in m³/s |
|
||||
| `cmd:Qd` | UI → process | manual demand m³/h (manual mode only) |
|
||||
| `cmd:ps-mode` | UI → process | `'levelbased' \| 'manual'` |
|
||||
| `cmd:mode` | Setup → process | per-pump `setMode` broadcast |
|
||||
| `cmd:station-startup / -shutdown / -estop` | UI → 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 |
|
||||
| `evt:ps` | process → UI | basin state, level, fill |
|
||||
| `evt:inflow` | Drivers → UI | live inflow value + active scenario |
|
||||
| `evt:tlm` | every EVOLV node → Telemetry | port-1 payload in `{measurement, fields, tags}` shape |
|
||||
| `setup:to-mgc` | Setup → process | one-shot MGC scaling/mode init |
|
||||
|
||||
## Per-pump physics feeder
|
||||
|
||||
Each pump has a `physics_<pump>` function node on the Process Plant tab. It receives:
|
||||
|
||||
1. The pump's own port-0 stream (state, predicted flow, predicted power).
|
||||
2. PS port-0 stream (basin level), fanned out by `ps_to_physics`.
|
||||
|
||||
It computes physically-coupled values for each sensor and emits them to the 4 measurement nodes:
|
||||
|
||||
| Sensor | Computation |
|
||||
|---|---|
|
||||
| Upstream pressure | `ρ g h` where `h = max(0, basinLevel − outflowLevel)`; pump suction sees the basin's hydrostatic head. |
|
||||
| Downstream pressure | Idle → static head only (12 m → 1177 mbar). Running → static + flow²-scaled dynamic head (up to ~2354 mbar at q=200 m³/h). |
|
||||
| Flow | Mirrors rotatingMachine's predicted flow with 1% Gaussian noise. Zero when the pump is idle. |
|
||||
| Power | Mirrors rotatingMachine's predicted power with 0.5% Gaussian noise. Zero when the pump is idle. |
|
||||
|
||||
Gaussian noise uses a 12-uniform-sum approximation (no external libs).
|
||||
|
||||
## Inflow scenarios
|
||||
|
||||
Pick a scenario on the **Realtime** dashboard page (group "Inflow"):
|
||||
|
||||
| Scenario | Behaviour |
|
||||
|---|---|
|
||||
| Constant | `q_h = baseline` (no modulation) |
|
||||
| Sine | `baseline · (1 + 0.5 · sin(2πt/240))` — period 4 min |
|
||||
| Diurnal | `baseline · (1 + 0.6 · sin(2πt/480 − π/2))` — period 8 min, peak offset |
|
||||
| Storm | 4-min cycle: rapid 5× ramp, then linear decay back to baseline |
|
||||
|
||||
Slider sets `baseline` in m³/h (0–250). The generator emits `q_in` to PS every second.
|
||||
|
||||
## Dashboard map
|
||||
|
||||
### Node-RED — `/dashboard`
|
||||
|
||||
Realtime page (`/dashboard/realtime`):
|
||||
|
||||
1. Inflow — slider, 4 scenario buttons, live value + active scenario label
|
||||
2. Station mode + commands — Auto/Manual switch, manual Qd slider, Start All / Stop All / Emergency Stop
|
||||
3. Basin realtime — direction, level, volume, fill %, net flow, time-to-full/empty, inflow, outflow, safety state, gauges (level + fill)
|
||||
4. MGC — total flow + power (text + gauges), efficiency
|
||||
5. Pump A / B / C — state, mode, controller %, flow, power, up/dn pressure (text), setpoint slider, Startup / Shutdown buttons
|
||||
|
||||
Trends page (`/dashboard/trends`) — 1-hour rolling windows:
|
||||
|
||||
- Basin level + fill %
|
||||
- Inflow / Outflow / Per-pump flow (one chart, multi-series)
|
||||
- Per-pump power
|
||||
- Per-pump up/dn pressure
|
||||
|
||||
### Grafana — `EVOLV / Pumping Station (complete)`
|
||||
|
||||
Two rows:
|
||||
|
||||
- **Realtime** — gauges for basin level + fill, stat panels for total flow / total power / per-pump state.
|
||||
- **Historic** — line charts for level + fill, inflow/outflow/net, per-pump flow + power (predicted), per-pump pressure, per-pump sensor flow + power (measured).
|
||||
|
||||
Default time range: last 15 minutes. Adjust with the Grafana picker for longer history.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
# 1. Bring up the stack
|
||||
docker compose up -d
|
||||
sleep 10 # wait for Node-RED ready
|
||||
|
||||
# 2. Deploy the flow
|
||||
curl -s -X POST http://localhost:1880/flows \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Node-RED-Deployment-Type: full' \
|
||||
--data-binary @examples/pumpingstation-complete-example/flow.json | jq .
|
||||
|
||||
# 3. Quick sanity check on Influx writes
|
||||
curl -s -X POST 'http://localhost:8086/api/v2/query?org=evolv' \
|
||||
-H 'Authorization: Token evolv-dev-token' \
|
||||
-H 'Accept: application/csv' \
|
||||
-H 'Content-type: application/vnd.flux' \
|
||||
--data 'from(bucket:"telemetry") |> range(start: -1m) |> count() |> group(columns: ["_measurement"])'
|
||||
```
|
||||
|
||||
You should see counts per measurement (`Pumping Station`, `Pump A`, `MGC — Pump Group`, the per-pump sensors, …) growing in real time.
|
||||
|
||||
## Regenerating `flow.json`
|
||||
|
||||
`flow.json` is generated from `build_flow.py`. Edit the Python (cleaner diff) and regenerate:
|
||||
|
||||
```bash
|
||||
cd examples/pumpingstation-complete-example
|
||||
python3 build_flow.py > flow.json
|
||||
```
|
||||
|
||||
The Python is the source of truth.
|
||||
|
||||
After regenerating, push the new flow into the running runtime:
|
||||
|
||||
```bash
|
||||
./scripts/sync-example.sh pumpingstation-complete-example
|
||||
```
|
||||
|
||||
## Projects + persistence (Node-RED)
|
||||
|
||||
The Docker stack uses a named volume (`evolv_nodered_data`) for `/data`, and Node-RED's **Projects** feature is enabled. Each folder under `examples/` is bootstrapped into `/data/projects/<name>/` on first container start with its own `git init` and a synthesized `package.json`. Switching between projects is two clicks in the editor: **menu → Projects → Open Project**.
|
||||
|
||||
| What you do | Where it lives | What persists |
|
||||
|---|---|---|
|
||||
| `docker compose down && up` | Container is recreated; named volume survives | Active flow + project list survive |
|
||||
| Edit a flow in the Node-RED editor | `/data/projects/<name>/flow.json` (in volume) | Until `docker compose down -v` |
|
||||
| Edit `examples/<name>/build_flow.py` then regenerate | `examples/<name>/flow.json` (in repo) | Always — it's in Git |
|
||||
| Run `scripts/sync-example.sh <name>` | Copies repo's `flow.json` → volume's project + reloads | Volume copy now matches repo |
|
||||
|
||||
### Adding a new example as a project
|
||||
|
||||
1. Create `examples/<your-name>/flow.json` (build it however you like — `build_flow.py` is one way).
|
||||
2. Restart the Node-RED container: `docker compose restart nodered`.
|
||||
3. Editor → Projects → Open Project → pick `<your-name>`.
|
||||
|
||||
The bootstrap is idempotent: existing projects in the volume aren't overwritten. To force a refresh from the repo: delete the project in the volume (`docker exec evolv-nodered rm -rf /data/projects/<name>`) and restart, or use `scripts/sync-example.sh` for a flow-only refresh.
|
||||
|
||||
To start fresh (wipe all volume state including flows, sessions, project history): `docker compose down -v`.
|
||||
|
||||
## Notable design choices
|
||||
|
||||
- **PS in `levelbased` mode** with `manual` mode toggleable from the UI. Levelbased = PS commands MGC by basin level; manual = operator drives MGC via the Qd slider.
|
||||
- **Inflow is operator-driven**, outflow is implicit (computed from pump activity). Single steerable knob (the Inflow group) keeps the demo focused.
|
||||
- **Sensors driven externally**, not by the measurement node's built-in simulator. The physics feeder is a function node on the Process Plant tab — disable it and sensors freeze, which is a useful failure mode to demonstrate.
|
||||
- **All EVOLV port 1 → one shared telemetry channel** (`evt:tlm`) → one writer. Adding a new EVOLV node anywhere in the flow only needs a new `lout_tlm_<id>` link-out + appending the id to `_all_tlm_lout_ids()` in `build_flow.py`.
|
||||
- **Dashboard pages split by concern, not data**: realtime widgets never share a page with historical charts.
|
||||
1909
examples/pumpingstation-complete-example/build_flow.py
Normal file
1909
examples/pumpingstation-complete-example/build_flow.py
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user