wiki: split per-node Home into Zone A (intuitive) + Reference-* siblings
New standard, pilot pass for pumpingStation. Sets the pattern the other 10 nodes will follow once we sign off on this one. Zone A (wiki/Home.md, ~180 lines): - one-sentence opener - "at a glance" 5-row fact table - "How it looks in Node-RED" — screenshot placeholder - "What it models" — embeds the existing basin-model.drawio.svg - "Try it" — 3-minute demo with curl-load command, click list, GIF placeholder - "Typical wiring" — two placeholder screenshots (standalone + integrated), no mermaid (per user direction) - "The five things you'll send" + sample Port-0 payload table - "Need more?" footer linking to Reference-* siblings Zone B (4 sibling pages): - Reference-Contracts.md — full topic contract + data model (AUTOGEN markers); config schema; child registration filters; unit policy - Reference-Architecture.md — 3-tier code layout; safety FSM (stateDiagram-v2); tick lifecycle (sequenceDiagram); output ports - Reference-Examples.md — 01-Basic / 02-Integration / 03-Dashboard walk-through with per-example screenshot + GIF placeholders; debug-recipes table - Reference-Limitations.md — implemented vs schema-only modes; basin-shape constraint; net-flow source caveat; alias-removal map Asset directory placeholders created: - wiki/_partial-screenshots/pumpingStation/.gitkeep - wiki/_partial-gifs/pumpingStation/.gitkeep - wiki/_partial-flows/pumpingStation/.gitkeep Abandoned per user direction (no longer linked, removed from source): - wiki/README.md - wiki/functional-description.md (377 lines retired) - wiki/modes/*.md (5 files retired) Diagrams kept in place (wiki/diagrams/*.drawio.svg) — referenced from Home and Reference-Architecture. package.json: wiki:contract + wiki:datamodel now target Reference-Contracts.md instead of Home.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
473
wiki/Home.md
473
wiki/Home.md
@@ -1,333 +1,178 @@
|
||||
# pumpingStation
|
||||
|
||||
> **Reflects code as of `530f84a` · regenerated `2026-05-11` via `npm run wiki:all`**
|
||||
> If this banner is stale, the page may be out of date. Treat as informative, not authoritative.
|
||||
  
|
||||
|
||||
## 1. What this node is
|
||||
A `pumpingStation` models a wet-well lift station: one basin with sensors, and one or more pumps that move water against an elevation difference. It integrates basin volume each tick, picks a control mode (level-based by default), and sends a demand setpoint to its pumps so the basin level stays inside its safe operating band.
|
||||
|
||||
**pumpingStation** is an S88 Process Cell that owns a wet-well basin and orchestrates the pumps that drain it. It tracks measured and predicted volume, evaluates safety interlocks (dry-run, overfill), and dispatches a control strategy that hands a demand setpoint to one or more downstream machine groups or individual pumps. Stateful (control mode) and tick-driven (1 s integrator). See [`wiki/functional-description.md`](functional-description) for the full behaviour spec.
|
||||
---
|
||||
|
||||
## 2. Position in the platform
|
||||
## At a glance
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
meas_lvl[measurement<br/>type=level<br/>position=atequipment]:::ctrl
|
||||
meas_in[measurement<br/>type=flow<br/>position=upstream]:::ctrl
|
||||
ps[pumpingStation<br/>Process Cell]:::pc
|
||||
mgc[machineGroupControl<br/>Unit]:::unit
|
||||
pump[rotatingMachine<br/>Equipment]:::equip
|
||||
| Thing | Value |
|
||||
|:---|:---|
|
||||
| What it represents | A wet-well lift station: a basin + N pumps |
|
||||
| S88 level | Process Cell |
|
||||
| Use it when | You need to lift water from a low point to a higher one, with sensors driving demand |
|
||||
| Don't use it for | Pressurised distribution networks (use a pumpingStation cascade or VGC instead), or a single pump with no basin (parent a `rotatingMachine` directly) |
|
||||
| Children it accepts | `measurement`, `machine`, `machinegroup`, `pumpingstation` |
|
||||
|
||||
meas_lvl -->|level.measured.atequipment| ps
|
||||
meas_in -->|flow.measured.upstream| ps
|
||||
pump -->|child.register| mgc
|
||||
mgc -->|child.register| ps
|
||||
mgc -->|flow.predicted.downstream| ps
|
||||
ps -->|set.demand| mgc
|
||||
classDef pc fill:#0c99d9,color:#fff
|
||||
classDef unit fill:#50a8d9,color:#000
|
||||
classDef equip fill:#86bbdd,color:#000
|
||||
classDef ctrl fill:#a9daee,color:#000
|
||||
---
|
||||
|
||||
## How it looks in Node-RED
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Screenshot needed.** Drop a `pumpingStation` node onto a fresh Node-RED canvas and capture:
|
||||
> - The node tile itself (its colour, badge text, label).
|
||||
> - The full edit dialog when you double-click it (basin geometry section visible).
|
||||
>
|
||||
> Save as `wiki/_partial-screenshots/pumpingStation/01-node-and-editor.png` (PNG, target 1200×800, optimise to ≤ 200 KB).
|
||||
> Then replace this callout with:
|
||||
>
|
||||
> ```markdown
|
||||
> 
|
||||
> ```
|
||||
|
||||
---
|
||||
|
||||
## What it models
|
||||
|
||||
A rectangular basin with measured inflow, measured (or pump-summed) outflow, and a level sensor. The diagram below is the live source; open it in [draw.io](https://app.diagrams.net/) to edit.
|
||||
|
||||

|
||||
|
||||
The basin has five horizontal reference lines that matter to the controller:
|
||||
|
||||
| Line | Role |
|
||||
|:---|:---|
|
||||
| `overflowLevel` | Physical weir crest. Above this level the basin is spilling. |
|
||||
| `maxLevel` | Demand saturates at 100 % at or above this level. |
|
||||
| `startLevel` | Falling-ramp returns to 0 % demand here; deadband upper bound. |
|
||||
| `minLevel` | Below this level the controller commands all pumps off. |
|
||||
| `dryRunLevel` | Pump-protection cutoff (safety layer, mode-independent). |
|
||||
|
||||
---
|
||||
|
||||
## Try it — 3-minute demo
|
||||
|
||||
Import the basic example flow, deploy, and watch the basin react to inject buttons.
|
||||
|
||||
```bash
|
||||
curl -X POST -H 'Content-Type: application/json' \
|
||||
--data @nodes/pumpingStation/examples/01-Basic.json \
|
||||
http://localhost:1880/flow
|
||||
```
|
||||
|
||||
S88 colours: Process Cell `#0c99d9`, Unit `#50a8d9`, Equipment `#86bbdd`, Control Module `#a9daee`. Source of truth: `.claude/rules/node-red-flow-layout.md §10.1`.
|
||||
> [!IMPORTANT]
|
||||
> **Flow screenshot needed.** Open the imported `01-Basic.json` flow in the Node-RED editor and capture the whole tab. The inject row should be visible on the left, the pumpingStation in the middle, the debug taps on the right.
|
||||
>
|
||||
> Save as `wiki/_partial-screenshots/pumpingStation/02-basic-flow.png` (PNG, target 1600×900, optimise to ≤ 250 KB).
|
||||
> Replace this callout with:
|
||||
>
|
||||
> ```markdown
|
||||
> 
|
||||
> ```
|
||||
|
||||
## 3. Capability matrix
|
||||
What to click in the dashboard after deploy:
|
||||
|
||||
| Capability | Status | Notes |
|
||||
|---|---|---|
|
||||
| Predicts basin volume from net flow | ✅ | Integrator seeded from `basin.minVol`; recomputes level each tick. |
|
||||
| Accepts measured level / volume / pressure / flow | ✅ | Routed via `measurementRouter` on child registration. |
|
||||
| Level-based control strategy | ✅ | Linear or log ramp between `startLevel` and `maxLevel`. |
|
||||
| Flow-based control strategy | ✅ | PID against `flowSetpoint`. |
|
||||
| Manual demand passthrough | ✅ | `set.demand` only honoured in `manual` mode. |
|
||||
| Dry-run safety interlock | ✅ | Shuts downstream pumps when volume < `minVol` while draining. Blocks control. |
|
||||
| Overfill safety interlock | ✅ | Shuts upstream equipment when volume > threshold while filling. Control keeps running. |
|
||||
| No-data panic | ✅ | Shuts ALL machines and blocks control when no volume reading is available. |
|
||||
| Cascaded sub-stations | ⚠️ | Accepted via `pumpingstation` softwareType but not exercised in production. |
|
||||
| pressureBased / powerBased / hybrid modes | ❌ | Enumerated in schema but not dispatched — only `levelbased`, `flowbased`, `manual`. |
|
||||
1. `set.mode = levelbased` → the controller switches to level-based mode.
|
||||
2. `set.inflow = 60 m³/h` → inflow is now feeding the basin.
|
||||
3. `cmd.calibrate.level = 1.5 m` → the volume integrator syncs to a known level.
|
||||
4. Watch Port 0 in the debug pane: level rises, predicted volume integrates, demand follows the curve.
|
||||
|
||||
## 4. Code map
|
||||
> [!IMPORTANT]
|
||||
> **GIF needed.** Record the dashboard reacting to the four clicks above. 15–25 seconds is enough. Use `peek` (Linux), LICEcap (Win/Mac), or any screen recorder; convert to GIF and optimise:
|
||||
>
|
||||
> ```bash
|
||||
> # if you started from an mp4:
|
||||
> ffmpeg -i raw.mp4 -vf "fps=15,scale=720:-1" -loop 0 stage.gif
|
||||
> gifsicle -O3 --lossy=80 stage.gif -o final.gif
|
||||
> ```
|
||||
>
|
||||
> Save as `wiki/_partial-gifs/pumpingStation/01-basic-demo.gif` (target ≤ 1 MB).
|
||||
> Replace this callout with:
|
||||
>
|
||||
> ```markdown
|
||||
> 
|
||||
> ```
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph nodeRED["nodeClass.js — adapter (BaseNodeAdapter)"]
|
||||
nc["buildDomainConfig()<br/>static DomainClass, commands<br/>static tickInterval = 1000 ms"]
|
||||
end
|
||||
subgraph domain["specificClass.js — orchestrator (BaseDomain)"]
|
||||
sc["PumpingStation.configure()<br/>declares ChildRouter rules<br/>tick() → flowAggregator → safety → control"]
|
||||
end
|
||||
subgraph concerns["src/ concern modules"]
|
||||
basin["basin/<br/>BasinGeometry · thresholdValidator"]
|
||||
measurement["measurement/<br/>flowAggregator · measurementRouter · calibration"]
|
||||
control["control/<br/>levelBased · flowBased · manual · dispatch"]
|
||||
safety["safety/<br/>SafetyController"]
|
||||
commands["commands/<br/>topic registry · handlers"]
|
||||
end
|
||||
nc --> sc
|
||||
sc --> basin
|
||||
sc --> measurement
|
||||
sc --> control
|
||||
sc --> safety
|
||||
nc --> commands
|
||||
---
|
||||
|
||||
## Typical wiring
|
||||
|
||||
The two patterns you'll see most.
|
||||
|
||||
### Standalone (`01-Basic.json`)
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Screenshot needed.** From the imported `01-Basic.json`, crop a tight view of just the inject column → pumpingStation → debug nodes. Skip the comment header.
|
||||
>
|
||||
> Save as `wiki/_partial-screenshots/pumpingStation/03-wiring-standalone.png` (PNG, target 1400×700).
|
||||
> Replace this callout with:
|
||||
>
|
||||
> ```markdown
|
||||
> 
|
||||
> ```
|
||||
|
||||
### With a measurement child and an MGC parent (`02-Integration.json`)
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Screenshot needed.** From the imported `02-Integration.json`, capture the whole tab. The measurement node feeding the pumpingStation should be visible on the left; the MGC with its two `rotatingMachine` pumps on the right.
|
||||
>
|
||||
> Save as `wiki/_partial-screenshots/pumpingStation/04-wiring-integrated.png` (PNG, target 1600×900).
|
||||
> Replace this callout with:
|
||||
>
|
||||
> ```markdown
|
||||
> 
|
||||
> ```
|
||||
|
||||
---
|
||||
|
||||
## The five things you'll send
|
||||
|
||||
| Topic | Payload | What it does |
|
||||
|:---|:---|:---|
|
||||
| `set.mode` | `"levelbased"` or `"manual"` | Switches control strategy. Manual exposes `set.demand` as the direct setpoint. |
|
||||
| `set.demand` | number, m³/h | Operator outflow setpoint. Honoured in `manual` mode. |
|
||||
| `set.inflow` | number, m³/h | Push a measured inflow into the basin balance (if you don't have a `measurement` child for inflow). |
|
||||
| `cmd.calibrate.level` | number, m | Sync the volume integrator to a known level reading. Useful at startup. |
|
||||
| `cmd.calibrate.volume` | number, m³ | Sync the volume integrator to a known volume reading. |
|
||||
|
||||
## What you'll see come out
|
||||
|
||||
Sample Port 0 message (delta-compressed — only changed fields each tick):
|
||||
|
||||
```json
|
||||
{
|
||||
"topic": "pumpingStation#PS1",
|
||||
"payload": {
|
||||
"level": 1.62,
|
||||
"volume": 32.4,
|
||||
"direction": "filling",
|
||||
"demand": 38,
|
||||
"safety": { "blocked": false },
|
||||
"etaSeconds": 412
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Module | Owns | Read first if you're changing… |
|
||||
|---|---|---|
|
||||
| `basin/` | Geometry, volume↔level conversion, threshold ordering | Capacity, level↔volume math, fill %. |
|
||||
| `measurement/` | Net-flow aggregation, predicted-volume integrator, calibration | Predicted volume / time-to-full. |
|
||||
| `control/` | Strategy dispatch (`levelbased`, `flowbased`, `manual`) | Demand calculation, mode behaviour. |
|
||||
| `safety/` | Dry-run + overfill rules, pump-shutdown side-effects | Safety envelope, alarm reactions. |
|
||||
| `commands/` | Input-topic registry and handlers | New input topics, payload validation. |
|
||||
| Field | Meaning |
|
||||
|:---|:---|
|
||||
| `level` | Current basin level (m). Measured if a level `measurement` is registered; predicted otherwise. |
|
||||
| `volume` | Integrated predicted volume (m³). |
|
||||
| `direction` | `filling` / `draining` / `steady` based on the flow dead-band. |
|
||||
| `demand` | What the station is asking its pumps to do (0–100 %). |
|
||||
| `safety.blocked` | True when the safety layer is overriding the control loop. |
|
||||
| `etaSeconds` | Predicted time to full (if filling) or empty (if draining). |
|
||||
|
||||
## 5. Topic contract
|
||||
---
|
||||
|
||||
> **Auto-generated** from `src/commands/index.js`. Do NOT hand-edit between the markers. Re-run `npm run wiki:contract`.
|
||||
## Need more?
|
||||
|
||||
<!-- BEGIN AUTOGEN: topic-contract -->
|
||||
| Page | What you'll find |
|
||||
|:---|:---|
|
||||
| [Reference — Contracts](Reference-Contracts) | Full topic contract, config schema, child registration filters |
|
||||
| [Reference — Architecture](Reference-Architecture) | Code map, state chart, lifecycle sequence, output ports |
|
||||
| [Reference — Examples](Reference-Examples) | All shipped example flows + Docker compose snippet + debug recipes |
|
||||
| [Reference — Limitations](Reference-Limitations) | When not to use this node, known limitations, open questions |
|
||||
|
||||
| Canonical topic | Aliases | Payload | Unit | Effect |
|
||||
|---|---|---|---|---|
|
||||
| `set.mode` | `changemode` | `string` | — | Switch the station between auto / manual control modes. |
|
||||
| `child.register` | `registerChild` | `string` | — | Register a child node (machine group, measurement, …) with this station. |
|
||||
| `cmd.calibrate.volume` | `calibratePredictedVolume` | `any` | `volume` (default `m3`) | Calibrate the predicted-volume integrator to a known basin volume. |
|
||||
| `cmd.calibrate.level` | `calibratePredictedLevel` | `any` | `length` (default `m`) | Calibrate the predicted-volume integrator to a known basin level. |
|
||||
| `set.inflow` | `q_in` | `any` | `volumeFlowRate` (default `m3/h`) | Push a measured inflow value into the basin balance. |
|
||||
| `set.outflow` | `q_out` | `any` | `volumeFlowRate` (default `m3/h`) | Push a measured outflow value into the basin balance. |
|
||||
| `set.demand` | `Qd` | `any` | `volumeFlowRate` (default `m3/h`) | Operator outflow demand setpoint for the station. |
|
||||
|
||||
<!-- END AUTOGEN: topic-contract -->
|
||||
|
||||
## 6. Child registration
|
||||
|
||||
Mirrors the `ChildRouter` declarations in `specificClass.js → configure()`.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph kids["accepted children (softwareType)"]
|
||||
m["measurement"]:::ctrl
|
||||
mach["machine<br/>(rotatingMachine)"]:::equip
|
||||
mgc["machinegroup<br/>(machineGroupControl)"]:::unit
|
||||
sub["pumpingstation<br/>(sub-station)"]:::pc
|
||||
end
|
||||
m -->|"<type>.measured.<position>"| route1[_subscribeMeasurement<br/>→ measurementRouter]
|
||||
mach -->|flow.predicted.out| route2[_subscribePredictedFlow<br/>+ flowAggregator]
|
||||
mgc -->|flow.predicted.out| route2
|
||||
sub -->|flow.predicted.out| route2
|
||||
route1 --> tick[tick / integrator]
|
||||
route2 --> tick
|
||||
classDef ctrl fill:#a9daee,color:#000
|
||||
classDef equip fill:#86bbdd,color:#000
|
||||
classDef unit fill:#50a8d9,color:#000
|
||||
classDef pc fill:#0c99d9,color:#fff
|
||||
```
|
||||
|
||||
| softwareType | onRegister side-effect | Subscribed events |
|
||||
|---|---|---|
|
||||
| `measurement` | `_subscribeMeasurement(child)` — writes to MeasurementContainer by type + position. | `<type>.measured.<position>` for any type (level, flow, pressure, …). |
|
||||
| `machine` | Added to `this.machines`. **Skipped when a `machinegroup` is present** — avoids double-counting predicted flow. | `flow.predicted.<in\|out>` per `positionVsParent`. |
|
||||
| `machinegroup` | Added to `this.machineGroups`. | `flow.predicted.<in\|out>`. |
|
||||
| `pumpingstation` | Added to `this.stations`. | `flow.predicted.<in\|out>`. |
|
||||
|
||||
## 7. Lifecycle — what one tick does
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant child as measurement / pump child
|
||||
participant ps as pumpingStation
|
||||
participant fa as flowAggregator
|
||||
participant sf as safetyController
|
||||
participant ctl as control strategy
|
||||
participant out as Port-0 output
|
||||
|
||||
child->>ps: data event (level.measured.atequipment / flow.predicted.out)
|
||||
ps->>ps: ChildRouter dispatches to _subscribeMeasurement / _subscribePredictedFlow
|
||||
Note over ps: every 1000 ms (static tickInterval = 1000)
|
||||
ps->>fa: tick() — net flow · ETA · predicted volume integrator
|
||||
ps->>sf: evaluate({direction, secondsRemaining})
|
||||
alt no-volume-data panic
|
||||
sf-->>ps: blocked=true, reason='no-volume-data'
|
||||
sf-->>ps: ALL machines shut down
|
||||
else dry-run (vol < minVol AND draining)
|
||||
sf-->>ps: blocked=true, reason='dry-run'
|
||||
sf-->>ps: downstream machines + machineGroups shut down
|
||||
else overfill (vol > threshold AND filling)
|
||||
sf-->>ps: blocked=false, reason='overfill'
|
||||
sf-->>ps: upstream machines + child stations shut down
|
||||
ps->>ctl: dispatch(mode, ctx, controlState)
|
||||
ctl-->>ps: percControl updated — pumps keep draining
|
||||
else safety clear
|
||||
ps->>ctl: dispatch(mode, ctx, controlState)
|
||||
ctl-->>ps: percControl updated
|
||||
end
|
||||
ps->>ps: notifyOutputChanged()
|
||||
ps->>out: msg{topic, payload (delta-compressed)}
|
||||
```
|
||||
|
||||
For control-strategy details see [`wiki/modes/`](modes/README).
|
||||
|
||||
## 8. Data model — `getOutput()`
|
||||
|
||||
What lands on Port 0. Built in `getOutput()`, then delta-compressed by `outputUtils.formatMsg`.
|
||||
|
||||
<!-- BEGIN AUTOGEN: data-model -->
|
||||
|
||||
| Key | Type | Unit | Sample |
|
||||
|---|---|---|---|
|
||||
| `direction` | string | — | `"steady"` |
|
||||
| `dryRunLevel` | number | — | `0.20400000000000001` |
|
||||
| `dryRunSafetyVol` | number | — | `0.20400000000000001` |
|
||||
| `flowSource` | null | — | `null` |
|
||||
| `heightBasin` | number | m | `1` |
|
||||
| `highVolumeSafetyLevel` | number | — | `2.45` |
|
||||
| `highVolumeSafetyVol` | number | — | `2.45` |
|
||||
| `inflowLevel` | number | m | `2` |
|
||||
| `inletPipeDiameter` | number | — | `0.4` |
|
||||
| `maxVol` | number | m3 | `1` |
|
||||
| `maxVolAtOverflow` | number | m3 | `2.5` |
|
||||
| `minHeightBasedOn` | string | — | `"outlet"` |
|
||||
| `minVol` | number | m3 | `0.2` |
|
||||
| `minVolAtInflow` | number | m3 | `2` |
|
||||
| `minVolAtOutflow` | number | m3 | `0.2` |
|
||||
| `outflowLevel` | number | m | `0.2` |
|
||||
| `outletPipeDiameter` | number | — | `0.4` |
|
||||
| `overflowLevel` | number | m | `2.5` |
|
||||
| `percControl` | number | % | `0` |
|
||||
| `predictedOverflowRate` | number | — | `0` |
|
||||
| `predictedOverflowVolume` | number | — | `0` |
|
||||
| `predictedUnderflowVolume` | number | — | `0` |
|
||||
| `surfaceArea` | number | m2 | `1` |
|
||||
| `timeleft` | null | s | `null` |
|
||||
| `volEmptyBasin` | number | m3 | `1` |
|
||||
| `volume.predicted.atequipment.wikigen-pumpingstation-id` | number | m3 | `0.2` |
|
||||
|
||||
<!-- END AUTOGEN: data-model -->
|
||||
|
||||
The `<nodeId>` segment of the MeasurementContainer key is the Node-RED node id assigned at deploy time; auto-gen substitutes a placeholder stub.
|
||||
|
||||
## 9. Configuration — editor form ↔ config keys
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph editor["Node-RED editor form"]
|
||||
f1[Basin: volume / height]
|
||||
f2[Levels: inflow / outflow / overflow]
|
||||
f3[Control mode]
|
||||
f4[Level-based setpoints: startLevel / stopLevel / minLevel / maxLevel]
|
||||
f5[Safety: dry-run % / high-volume %]
|
||||
end
|
||||
subgraph config["Domain config slice"]
|
||||
c1[basin.volume<br/>basin.height]
|
||||
c2[basin.inflowLevel<br/>basin.outflowLevel<br/>basin.overflowLevel]
|
||||
c3[control.mode]
|
||||
c4[control.levelbased.startLevel<br/>control.levelbased.stopLevel<br/>control.levelbased.minLevel<br/>control.levelbased.maxLevel]
|
||||
c5[safety.dryRunThresholdPercent<br/>safety.highVolumeSafetyThresholdPercent]
|
||||
end
|
||||
f1 --> c1
|
||||
f2 --> c2
|
||||
f3 --> c3
|
||||
f4 --> c4
|
||||
f5 --> c5
|
||||
```
|
||||
|
||||
| Form field | Config key | Default | Range | Where used |
|
||||
|---|---|---|---|---|
|
||||
| `basinVolume` | `basin.volume` | `1` | > 0 (m³) | `BasinGeometry` |
|
||||
| `basinHeight` | `basin.height` | `1` | > 0 (m) | `BasinGeometry` |
|
||||
| `inflowLevel` | `basin.inflowLevel` | `0.8` | ≥ 0 (m) | threshold validator, control ramp foot |
|
||||
| `outflowLevel` | `basin.outflowLevel` | `0.2` | ≥ 0 (m) | dead-volume floor |
|
||||
| `overflowLevel` | `basin.overflowLevel` | `0.9` | > 0 (m) | overfill safety ceiling |
|
||||
| `controlMode` | `control.mode` | `levelbased` | enum | `control/dispatch` |
|
||||
| `levelCurveType` | `control.levelbased.curveType` | `linear` | `linear` \| `log` | `levelBased.run` |
|
||||
| `logCurveFactor` | `control.levelbased.logCurveFactor` | `9` | > 0 | log-curve steepness |
|
||||
| `enableShiftedRamp` | `control.levelbased.enableShiftedRamp` | `false` | bool | hysteresis ramp |
|
||||
| `startLevel` | `control.levelbased.startLevel` | `null` | ≥ 0 (m) | ramp zero-point |
|
||||
| `stopLevel` | `control.levelbased.stopLevel` | `null` | ≥ 0 (m) | Schmitt-trigger off threshold |
|
||||
| `minLevel` | `control.levelbased.minLevel` | `null` | ≥ 0 (m) | `levelBased.run` |
|
||||
| `maxLevel` | `control.levelbased.maxLevel` | `null` | ≤ overflowLevel (m) | ramp 100 % point |
|
||||
| `flowSetpoint` | `control.flowbased.setpoint` | `null` | ≥ 0 (m³/h) | flow-PID target |
|
||||
| `enableDryRunProtection` | `safety.enableDryRunProtection` | `true` | bool | `SafetyController._dryRunRule` |
|
||||
| `dryRunThresholdPercent` | `safety.dryRunThresholdPercent` | `2` | 0–100 % | dry-run trip volume |
|
||||
| `enableHighVolumeSafety` | `safety.enableHighVolumeSafety` | `true` | bool | `SafetyController._overfillRule` |
|
||||
| `highVolumeSafetyThresholdPercent` | `safety.highVolumeSafetyThresholdPercent` | `98` | 0–100 % | overfill trip volume |
|
||||
| `timeleftToFullOrEmptyThresholdSeconds` | `safety.timeleftToFullOrEmptyThresholdSeconds` | `0` | ≥ 0 (s) | ETA-based pre-trip guard |
|
||||
|
||||
> `enableOverfillProtection` and `overfillThresholdPercent` are **deprecated aliases** still accepted by `SafetyController` for back-compat. Use `enableHighVolumeSafety` and `highVolumeSafetyThresholdPercent` in new flows. See `OPEN_QUESTIONS.md` (B1.2 resolved).
|
||||
|
||||
## 10. State chart
|
||||
|
||||
pumpingStation has two orthogonal state vectors: **control mode** (operator-driven, persistent) and **safety state** (data-driven, evaluated every tick). The e-stop path is the no-volume-data panic that shuts all machines independently.
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
state ControlMode {
|
||||
[*] --> levelbased
|
||||
levelbased --> flowbased : set.mode
|
||||
flowbased --> manual : set.mode
|
||||
manual --> levelbased : set.mode
|
||||
manual --> none : set.mode
|
||||
levelbased --> none : set.mode
|
||||
none --> levelbased : set.mode
|
||||
}
|
||||
|
||||
state SafetyState {
|
||||
[*] --> nominal
|
||||
nominal --> dryRun : vol < minVol AND draining
|
||||
nominal --> overfill : vol > highVolThreshold AND filling
|
||||
nominal --> panic : no volume reading
|
||||
dryRun --> nominal : vol ≥ minVol
|
||||
overfill --> nominal : vol ≤ highVolThreshold
|
||||
panic --> nominal : volume reading restored
|
||||
}
|
||||
```
|
||||
|
||||
| Safety state | `blocked` | Control dispatch | Side-effects |
|
||||
|---|---|---|---|
|
||||
| `nominal` | false | runs normally | — |
|
||||
| `dryRun` | **true** | **skipped** | downstream machines + machineGroups shut down |
|
||||
| `overfill` | false | runs (pumps must drain) | upstream machines + child stations shut down |
|
||||
| `panic` | **true** | **skipped** | **ALL** machines shut down |
|
||||
|
||||
`dryRun` is triggered when `direction='draining'` AND vol < `minVol × (1 + dryRunThresholdPercent/100)`.
|
||||
`overfill` is triggered when `direction='filling'` AND vol > `maxVolAtOverflow × (highVolumeSafetyThresholdPercent/100)`.
|
||||
|
||||
## 11. Examples
|
||||
|
||||
All three tiers are written and runnable. Import any file via the Node-RED editor or the Admin API.
|
||||
|
||||
| Tier | File | What it shows | Status |
|
||||
|---|---|---|---|
|
||||
| Basic | `examples/01-Basic.json` | Single pumpingStation driven by inject nodes — no parent, no dashboard. Try `set.inflow`, `set.mode`, `cmd.calibrate.volume`. | ✅ |
|
||||
| Integration | `examples/02-Integration.json` | pumpingStation + `machineGroupControl` + 2 `rotatingMachine` pumps + level `measurement`. Demonstrates Phase-2 parent/child handshake and `levelbased` control driving real pumps. | ✅ |
|
||||
| Dashboard | `examples/03-Dashboard.json` | Tier 2 plumbing + FlowFuse Dashboard 2.0 page — 3 charts (flow / level / volume %), mode dropdown, demand slider. | ✅ |
|
||||
| Headless | `examples/standalone-demo.js` | Node.js-only simulator, no Node-RED required. | ✅ |
|
||||
|
||||
See `examples/README.md` for layout conventions (link channels, lane positions, group boxes).
|
||||
|
||||
## 12. Debug recipes
|
||||
|
||||
| Symptom | First thing to check | Where to look |
|
||||
|---|---|---|
|
||||
| Status badge stuck on `❔ 0.0%` | No volume/level measurement registered yet. Watch Port 2. | Editor debug tap on Port 2 + `_subscribeMeasurement` log line. |
|
||||
| `direction` always `steady` | Net flow inside `general.flowThreshold` dead-band (default 0.0001 m³/s ≈ 0.36 m³/h). | `flowAggregator.deriveDirection`. |
|
||||
| `set.demand` ignored | Mode isn't `manual`. Confirm with `set.mode=manual` first. | `handlers.setDemand` debug log. |
|
||||
| Predicted volume drifts off measured | Integrator needs a calibration anchor. Fire `cmd.calibrate.volume` with a known basin volume. | `measurement/calibration.js`. |
|
||||
| Pumps don't stop on dry-run | `safety.enableDryRunProtection` must be `true` AND `direction` must be `'draining'`. | `SafetyController._dryRunRule`. |
|
||||
| Threshold-ordering warnings on startup | `validateThresholdOrdering` detected violations (e.g. `inflowLevel > overflowLevel`). | `basin/thresholdValidator.js`. |
|
||||
| All machines shut down immediately | No volume reading reached the node — panic path in SafetyController. Check child registration sequence. | `SafetyController.evaluate` line 59. |
|
||||
|
||||
> Never ship `enableLog: 'debug'` in a demo — fills the container log within seconds and obscures real errors. Use only for live debugging.
|
||||
|
||||
## 13. When you would NOT use this node
|
||||
|
||||
- Use `rotatingMachine` directly for a single pump with no basin model. pumpingStation adds overhead that pays off only when you need predicted volume, time-to-full, or multi-pump orchestration.
|
||||
- Don't use pumpingStation to schedule a fixed pump rota. Its control modes are reactive (level / flow / manual demand), not calendar-driven. Use an external scheduler and wire it in via `set.demand`.
|
||||
- Skip pumpingStation if you only need flow or pressure measurements with no wet-well state. A bare `machineGroupControl` is lighter when the basin is modelled elsewhere or not at all.
|
||||
|
||||
## 14. Known limitations / current issues
|
||||
|
||||
| # | Issue | Tracked in |
|
||||
|---|---|---|
|
||||
| 1 | Cascaded `pumpingstation` children accepted but semantics of nested stations are not test-covered in production scenarios. | TBD — exercise in Docker E2E before promoting. |
|
||||
| 2 | `pressureBased`, `percentageBased`, `powerBased`, and `hybrid` are listed in the config enum but not dispatched — only `levelbased`, `flowbased`, `manual` are implemented. | `control/index.js` |
|
||||
| 3 | Predicted-volume integrator drifts over long horizons without a measured-level calibration source. `cmd.calibrate.volume` is operator-triggered, not automatic. | Operator procedure; auto-calibration from level sensor is future work. |
|
||||
| 4 | `enableOverfillProtection` / `overfillThresholdPercent` deprecated aliases still accepted by `SafetyController` (back-compat). Remove after one release cycle. | B1.2 resolved in `OPEN_QUESTIONS.md`. |
|
||||
[EVOLV master wiki](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Home) · [Topology Patterns](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topology-Patterns) · [Topic Conventions](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions)
|
||||
|
||||
Reference in New Issue
Block a user