Add wiki/ folder with functional description + draw.io diagrams

Moves documentation into the code repo so code, docs, and diagrams
version-lock and review together. Previous location was
pumpingStation.wiki.git; that will shrink to a pointer.

Contents:
- wiki/README.md — doc index
- wiki/functional-description.md — operator-facing reference derived
  from src/specificClass.js: basin model, net-flow selection,
  level-based control zones, safety interlocks, registration topology
- wiki/diagrams/ — editable draw.io sources paired with SVG exports
  (basin-model, control-zones, safety-rules) + README with the
  open/edit/export/commit workflow

The .drawio files are rough starters; iterate in draw.io and re-export.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
znetsixe
2026-04-22 12:19:26 +02:00
parent 5e2ebe4d96
commit 0ff55f5e9c
9 changed files with 853 additions and 0 deletions

15
wiki/README.md Normal file
View File

@@ -0,0 +1,15 @@
# pumpingStation — Documentation
All docs and diagrams for this node live in this folder so they version-lock with the code they describe.
## Pages
- **[Functional Description](functional-description.md)** — operator-facing reference derived from `src/specificClass.js`: basin model, net-flow selection, level-based control zones, safety interlocks, registration topology.
## Diagrams
Editable draw.io sources live in [`diagrams/`](diagrams/). See [`diagrams/README.md`](diagrams/README.md) for the editing workflow — open `.drawio` files in [draw.io](https://app.diagrams.net/), export to `.drawio.svg`, commit both.
## Part of
This node is a git submodule of [EVOLV](https://gitea.wbd-rd.nl/RnD/EVOLV). The EVOLV superproject has its own [`wiki/`](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/main/wiki) with platform-level docs (architecture, concepts, shared manuals).

69
wiki/diagrams/README.md Normal file
View File

@@ -0,0 +1,69 @@
# Diagrams
Editable source diagrams for the pumpingStation wiki. Each diagram is a **`.drawio` + `.drawio.svg` pair**, so anyone can edit the source in [draw.io](https://app.diagrams.net/) without touching any Markdown.
## Why two files?
| File | Role |
|---|---|
| `<name>.drawio` | Native draw.io XML. The canonical source. |
| `<name>.drawio.svg` | SVG export of the same diagram (with source embedded). What the wiki actually renders, and what round-trips back into draw.io. |
Checking both in means the wiki renders for everyone, and the next editor picks up from exactly where the last one left off.
## Editing workflow
1. **Clone** the repo (you likely already have it if you're editing):
```bash
git clone https://gitea.wbd-rd.nl/RnD/pumpingStation.git
cd pumpingStation/wiki/diagrams
```
2. **Open** the `.drawio` file in draw.io:
- Web: [app.diagrams.net](https://app.diagrams.net/) → *Open Existing Diagram*, or drag-and-drop.
- Desktop: [drawio-desktop](https://github.com/jgraph/drawio-desktop/releases).
3. **Edit** — move shapes, change labels, adjust layout.
4. **Export** to SVG with the source embedded:
- `File → Export as → SVG…`
- Check **Include a copy of my diagram** ← this is what lets future edits round-trip through the SVG.
- Save next to the source as `<name>.drawio.svg` (overwrite).
5. **Commit & push** both files:
```bash
git add wiki/diagrams/<name>.drawio wiki/diagrams/<name>.drawio.svg
git commit -m "Update <name>: <what changed>"
git push
```
## Referencing a diagram from a wiki page
In any Markdown page under `wiki/`:
```markdown
![Basin model](diagrams/basin-model.drawio.svg)
```
Use a descriptive `alt` text; it's the fallback if the SVG fails and it shows up in exports.
## Naming
- kebab-case, one concept per diagram.
- Current diagrams:
| Diagram | Shows |
|---|---|
| `basin-model` | Physical basin cross-section — walls, pipes at their real heights, control thresholds cutting across, zone labels |
| `control-zones` | Vertical level axis ("thermometer") for `levelbased` mode — STOP / DEAD ZONE / RUN with demand ramp |
| `safety-rules` | Dry-run vs overfill rule asymmetry — which children stop, which keep running |
## Making a brand-new diagram
1. Open draw.io, start blank.
2. Draw it.
3. `File → Save As…` → `wiki/diagrams/<name>.drawio`.
4. `File → Export as → SVG…` with **Include a copy of my diagram** checked → save as `wiki/diagrams/<name>.drawio.svg`.
5. Reference from the wiki page with `![alt](diagrams/<name>.drawio.svg)`.
6. Add an entry to the table above.
7. Commit all three files together (`.drawio`, `.drawio.svg`, updated `.md`).
## These starters are rough
The `.drawio` files committed here by Claude are **placeholders** — layout is approximate, colors and fonts are defaults, no fine alignment. They compile to a valid SVG you can see rendered on the page, but they're meant to be a starting point. Open them in draw.io, refine the layout, re-export.

View File

@@ -0,0 +1,109 @@
<mxfile host="app.diagrams.net" modified="2026-04-22T12:00:00.000Z" agent="Claude Code placeholder" etag="initial" version="22.0.0" type="device">
<diagram name="basin-model" id="basinModel">
<mxGraphModel dx="1200" dy="900" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="900" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="title" value="Basin model — physical layout + control thresholds" style="text;html=1;fontSize=16;fontStyle=1;align=center;" vertex="1" parent="1">
<mxGeometry x="200" y="20" width="500" height="30" as="geometry" />
</mxCell>
<mxCell id="tank" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#E6F2FF;strokeColor=#000000;strokeWidth=2;" vertex="1" parent="1">
<mxGeometry x="300" y="80" width="260" height="520" as="geometry" />
</mxCell>
<mxCell id="deadvol" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#9FC5E8;strokeColor=none;" vertex="1" parent="1">
<mxGeometry x="302" y="550" width="256" height="48" as="geometry" />
</mxCell>
<mxCell id="freeboard_label" value="freeboard" style="text;html=1;fontSize=11;fontStyle=2;align=center;" vertex="1" parent="1">
<mxGeometry x="310" y="90" width="240" height="20" as="geometry" />
</mxCell>
<mxCell id="overflow_line" value="" style="endArrow=none;html=1;strokeColor=#B22222;dashed=1;strokeWidth=2;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="250" y="145" as="sourcePoint" />
<mxPoint x="620" y="145" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="overflow_label_l" value="heightOverflow" style="text;html=1;fontSize=12;align=right;fontColor=#B22222;" vertex="1" parent="1">
<mxGeometry x="140" y="130" width="100" height="20" as="geometry" />
</mxCell>
<mxCell id="overflow_label_r" value="spill → measure" style="text;html=1;fontSize=12;align=left;fontColor=#B22222;" vertex="1" parent="1">
<mxGeometry x="630" y="130" width="140" height="20" as="geometry" />
</mxCell>
<mxCell id="maxflow_line" value="" style="endArrow=none;html=1;strokeColor=#D68910;dashed=1;strokeWidth=2;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="250" y="200" as="sourcePoint" />
<mxPoint x="620" y="200" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="maxflow_label_l" value="maxFlowLevel" style="text;html=1;fontSize=12;align=right;fontColor=#D68910;" vertex="1" parent="1">
<mxGeometry x="140" y="185" width="100" height="20" as="geometry" />
</mxCell>
<mxCell id="scaling_label" value="SCALING RANGE&#10;(levelbased: demand ramps 0→100%)" style="text;html=1;fontSize=11;fontStyle=2;align=center;" vertex="1" parent="1">
<mxGeometry x="310" y="255" width="240" height="40" as="geometry" />
</mxCell>
<mxCell id="startlevel_line" value="" style="endArrow=none;html=1;strokeColor=#1E8449;dashed=1;strokeWidth=2;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="250" y="345" as="sourcePoint" />
<mxPoint x="620" y="345" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="startlevel_label_l" value="startLevel" style="text;html=1;fontSize=12;align=right;fontColor=#1E8449;" vertex="1" parent="1">
<mxGeometry x="140" y="330" width="100" height="20" as="geometry" />
</mxCell>
<mxCell id="deadzone_label" value="DEAD ZONE&#10;(hysteresis — keep last cmd)" style="text;html=1;fontSize=11;fontStyle=2;align=center;" vertex="1" parent="1">
<mxGeometry x="310" y="360" width="240" height="40" as="geometry" />
</mxCell>
<mxCell id="inflow_arrow" value="" style="endArrow=classic;html=1;strokeColor=#1F4E79;strokeWidth=3;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="150" y="410" as="sourcePoint" />
<mxPoint x="300" y="410" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="inflow_label" value="INFLOW" style="text;html=1;fontSize=13;fontStyle=1;align=left;fontColor=#1F4E79;" vertex="1" parent="1">
<mxGeometry x="90" y="395" width="70" height="20" as="geometry" />
</mxCell>
<mxCell id="inlet_label" value="heightInlet" style="text;html=1;fontSize=12;align=left;fontColor=#1F4E79;" vertex="1" parent="1">
<mxGeometry x="570" y="400" width="90" height="20" as="geometry" />
</mxCell>
<mxCell id="stoplevel_line" value="" style="endArrow=none;html=1;strokeColor=#6C3483;dashed=1;strokeWidth=2;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="250" y="465" as="sourcePoint" />
<mxPoint x="620" y="465" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="stoplevel_label_l" value="stopLevel" style="text;html=1;fontSize=12;align=right;fontColor=#6C3483;" vertex="1" parent="1">
<mxGeometry x="140" y="450" width="100" height="20" as="geometry" />
</mxCell>
<mxCell id="stoplevel_label_r" value="unconditional STOP" style="text;html=1;fontSize=12;align=left;fontColor=#6C3483;" vertex="1" parent="1">
<mxGeometry x="630" y="450" width="160" height="20" as="geometry" />
</mxCell>
<mxCell id="buffer_label" value="BUFFER" style="text;html=1;fontSize=11;fontStyle=2;align=center;" vertex="1" parent="1">
<mxGeometry x="310" y="490" width="240" height="20" as="geometry" />
</mxCell>
<mxCell id="outflow_arrow" value="" style="endArrow=classic;html=1;strokeColor=#1F4E79;strokeWidth=3;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="560" y="540" as="sourcePoint" />
<mxPoint x="720" y="540" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="outflow_label" value="OUTFLOW" style="text;html=1;fontSize=13;fontStyle=1;align=left;fontColor=#1F4E79;" vertex="1" parent="1">
<mxGeometry x="730" y="525" width="80" height="20" as="geometry" />
</mxCell>
<mxCell id="outlet_label_l" value="heightOutlet" style="text;html=1;fontSize=12;align=right;fontColor=#B22222;" vertex="1" parent="1">
<mxGeometry x="140" y="525" width="100" height="20" as="geometry" />
</mxCell>
<mxCell id="outlet_label_r" value="dry-run trip" style="text;html=1;fontSize=12;align=left;fontColor=#B22222;" vertex="1" parent="1">
<mxGeometry x="730" y="550" width="120" height="20" as="geometry" />
</mxCell>
<mxCell id="deadvol_label" value="dead volume" style="text;html=1;fontSize=11;fontStyle=2;align=center;" vertex="1" parent="1">
<mxGeometry x="310" y="560" width="240" height="20" as="geometry" />
</mxCell>
<mxCell id="floor_label" value="floor (0)" style="text;html=1;fontSize=11;align=right;" vertex="1" parent="1">
<mxGeometry x="190" y="590" width="50" height="20" as="geometry" />
</mxCell>
<mxCell id="basin_label" value="heightBasin" style="text;html=1;fontSize=11;align=right;" vertex="1" parent="1">
<mxGeometry x="180" y="70" width="60" height="20" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 850 660" font-family="Arial, sans-serif" font-size="13">
<title>Basin model — physical layout + control thresholds</title>
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#1F4E79" />
</marker>
</defs>
<text x="425" y="30" text-anchor="middle" font-weight="bold" font-size="16">Basin model — physical layout + control thresholds</text>
<!-- Tank body -->
<rect x="300" y="80" width="260" height="520" fill="#E6F2FF" stroke="#000" stroke-width="2" />
<!-- Dead volume (darker band at the bottom) -->
<rect x="302" y="550" width="256" height="48" fill="#9FC5E8" />
<!-- Zone labels inside the tank -->
<text x="430" y="106" text-anchor="middle" font-style="italic" font-size="12">freeboard</text>
<text x="430" y="268" text-anchor="middle" font-style="italic" font-size="12">SCALING RANGE</text>
<text x="430" y="284" text-anchor="middle" font-style="italic" font-size="11">(levelbased: demand ramps 0 → 100 %)</text>
<text x="430" y="378" text-anchor="middle" font-style="italic" font-size="12">DEAD ZONE</text>
<text x="430" y="394" text-anchor="middle" font-style="italic" font-size="11">(hysteresis — keep last cmd)</text>
<text x="430" y="502" text-anchor="middle" font-style="italic" font-size="12">BUFFER</text>
<text x="430" y="576" text-anchor="middle" font-style="italic" font-size="11" fill="#333">dead volume</text>
<!-- heightBasin (top, solid) -->
<line x1="250" y1="80" x2="560" y2="80" stroke="#000" stroke-width="2" />
<text x="240" y="84" text-anchor="end" font-size="11">heightBasin</text>
<!-- heightOverflow (dashed red) -->
<line x1="250" y1="145" x2="620" y2="145" stroke="#B22222" stroke-dasharray="6 3" stroke-width="1.5" />
<text x="240" y="141" text-anchor="end" fill="#B22222" font-size="12">heightOverflow</text>
<text x="630" y="141" text-anchor="start" fill="#B22222" font-size="12">→ spill → measure</text>
<!-- maxFlowLevel (dashed orange) -->
<line x1="250" y1="200" x2="620" y2="200" stroke="#D68910" stroke-dasharray="6 3" stroke-width="1.5" />
<text x="240" y="196" text-anchor="end" fill="#D68910" font-size="12">maxFlowLevel</text>
<!-- startLevel (dashed green) -->
<line x1="250" y1="345" x2="620" y2="345" stroke="#1E8449" stroke-dasharray="6 3" stroke-width="1.5" />
<text x="240" y="341" text-anchor="end" fill="#1E8449" font-size="12">startLevel</text>
<!-- heightInlet (solid blue) with INFLOW pipe -->
<line x1="150" y1="410" x2="296" y2="410" stroke="#1F4E79" stroke-width="3" marker-end="url(#arrow)" />
<text x="140" y="406" text-anchor="end" font-weight="bold" fill="#1F4E79">INFLOW</text>
<text x="570" y="414" text-anchor="start" fill="#1F4E79" font-size="12">heightInlet</text>
<!-- stopLevel (dashed purple) -->
<line x1="250" y1="465" x2="620" y2="465" stroke="#6C3483" stroke-dasharray="6 3" stroke-width="1.5" />
<text x="240" y="461" text-anchor="end" fill="#6C3483" font-size="12">stopLevel</text>
<text x="630" y="461" text-anchor="start" fill="#6C3483" font-size="12">→ unconditional STOP</text>
<!-- heightOutlet (solid red) with OUTFLOW pipe -->
<line x1="564" y1="540" x2="720" y2="540" stroke="#1F4E79" stroke-width="3" marker-end="url(#arrow)" />
<text x="730" y="536" text-anchor="start" font-weight="bold" fill="#1F4E79">OUTFLOW</text>
<text x="240" y="536" text-anchor="end" fill="#B22222" font-size="12">heightOutlet</text>
<text x="730" y="552" text-anchor="start" fill="#B22222" font-size="12">→ dry-run trip</text>
<!-- floor -->
<line x1="250" y1="600" x2="560" y2="600" stroke="#000" stroke-width="2" />
<text x="240" y="604" text-anchor="end" font-size="11">floor (0)</text>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,102 @@
<mxfile host="app.diagrams.net" modified="2026-04-22T12:00:00.000Z" agent="Claude Code placeholder" etag="initial" version="22.0.0" type="device">
<diagram name="control-zones" id="controlZones">
<mxGraphModel dx="1000" dy="800" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="700" pageHeight="800" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="title" value="levelbased mode — three zones" style="text;html=1;fontSize=16;fontStyle=1;align=center;" vertex="1" parent="1">
<mxGeometry x="100" y="20" width="500" height="30" as="geometry" />
</mxCell>
<mxCell id="axis" value="" style="endArrow=classic;html=1;strokeColor=#000;strokeWidth=2;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="280" y="600" as="sourcePoint" />
<mxPoint x="280" y="80" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="axis_label" value="level" style="text;html=1;fontSize=13;fontStyle=1;align=left;" vertex="1" parent="1">
<mxGeometry x="240" y="60" width="50" height="20" as="geometry" />
</mxCell>
<mxCell id="overflow" value="heightOverflow — weir crest (spill → measure)" style="text;html=1;fontSize=12;align=left;fontColor=#B22222;" vertex="1" parent="1">
<mxGeometry x="300" y="130" width="380" height="20" as="geometry" />
</mxCell>
<mxCell id="overflow_tick" value="" style="endArrow=none;html=1;strokeColor=#B22222;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="270" y="140" as="sourcePoint" />
<mxPoint x="290" y="140" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="run_band" value="RUN — linear 0 → 100 %" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#E8F5E9;strokeColor=#1E8449;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="300" y="160" width="220" height="110" as="geometry" />
</mxCell>
<mxCell id="maxflow" value="maxFlowLevel — 100 % demand" style="text;html=1;fontSize=12;align=left;fontColor=#D68910;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="300" y="265" width="300" height="20" as="geometry" />
</mxCell>
<mxCell id="maxflow_tick" value="" style="endArrow=none;html=1;strokeColor=#D68910;strokeWidth=2;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="265" y="275" as="sourcePoint" />
<mxPoint x="295" y="275" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="ramp_label" value="(ramp — demand scales linearly with level)" style="text;html=1;fontSize=11;align=left;fontStyle=2;" vertex="1" parent="1">
<mxGeometry x="300" y="300" width="320" height="20" as="geometry" />
</mxCell>
<mxCell id="startlevel" value="startLevel — 0 % demand (ramp starts)" style="text;html=1;fontSize=12;align=left;fontColor=#1E8449;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="300" y="335" width="340" height="20" as="geometry" />
</mxCell>
<mxCell id="start_tick" value="" style="endArrow=none;html=1;strokeColor=#1E8449;strokeWidth=2;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="265" y="345" as="sourcePoint" />
<mxPoint x="295" y="345" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="dead_band" value="DEAD ZONE — hysteresis, keep last cmd" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#FFF8E1;strokeColor=#F57C00;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="300" y="360" width="220" height="80" as="geometry" />
</mxCell>
<mxCell id="inlet" value="heightInlet — inflow pipe" style="text;html=1;fontSize=12;align=left;fontColor=#1F4E79;" vertex="1" parent="1">
<mxGeometry x="300" y="395" width="300" height="20" as="geometry" />
</mxCell>
<mxCell id="inlet_tick" value="" style="endArrow=none;html=1;strokeColor=#1F4E79;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="270" y="405" as="sourcePoint" />
<mxPoint x="290" y="405" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="stoplevel" value="stopLevel — unconditional STOP" style="text;html=1;fontSize=12;align=left;fontColor=#6C3483;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="300" y="440" width="300" height="20" as="geometry" />
</mxCell>
<mxCell id="stop_tick" value="" style="endArrow=none;html=1;strokeColor=#6C3483;strokeWidth=2;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="265" y="450" as="sourcePoint" />
<mxPoint x="295" y="450" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="stop_band" value="pumps OFF (MGC shutdown)" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#F4ECF7;strokeColor=#6C3483;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="300" y="465" width="220" height="80" as="geometry" />
</mxCell>
<mxCell id="outlet" value="heightOutlet — outflow pipe (dry-run trip here)" style="text;html=1;fontSize=12;align=left;fontColor=#B22222;" vertex="1" parent="1">
<mxGeometry x="300" y="510" width="360" height="20" as="geometry" />
</mxCell>
<mxCell id="outlet_tick" value="" style="endArrow=none;html=1;strokeColor=#B22222;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="270" y="520" as="sourcePoint" />
<mxPoint x="290" y="520" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="floor" value="0 (floor)" style="text;html=1;fontSize=11;align=left;" vertex="1" parent="1">
<mxGeometry x="300" y="580" width="60" height="20" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 660" font-family="Arial, sans-serif" font-size="13">
<title>levelbased mode — three zones</title>
<defs>
<marker id="arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#000" />
</marker>
</defs>
<text x="350" y="30" text-anchor="middle" font-weight="bold" font-size="16">levelbased mode — three zones</text>
<!-- Vertical level axis -->
<line x1="280" y1="600" x2="280" y2="80" stroke="#000" stroke-width="2" marker-end="url(#arr)" />
<text x="260" y="75" text-anchor="end" font-weight="bold" font-size="13">level</text>
<!-- heightOverflow -->
<line x1="270" y1="140" x2="290" y2="140" stroke="#B22222" stroke-width="2" />
<text x="300" y="144" fill="#B22222" font-size="12">heightOverflow — weir crest (spill → measure)</text>
<!-- RUN band -->
<rect x="300" y="160" width="240" height="110" fill="#E8F5E9" stroke="#1E8449" />
<text x="420" y="220" text-anchor="middle" font-size="13" fill="#1E8449" font-weight="bold">RUN</text>
<text x="420" y="238" text-anchor="middle" font-size="12" fill="#1E8449">linear 0 → 100 %</text>
<!-- maxFlowLevel -->
<line x1="265" y1="275" x2="295" y2="275" stroke="#D68910" stroke-width="3" />
<text x="305" y="279" fill="#D68910" font-size="12" font-weight="bold">maxFlowLevel — 100 % demand</text>
<!-- Ramp label -->
<text x="305" y="314" font-size="11" font-style="italic">(ramp — demand scales linearly with level)</text>
<!-- startLevel -->
<line x1="265" y1="345" x2="295" y2="345" stroke="#1E8449" stroke-width="3" />
<text x="305" y="349" fill="#1E8449" font-size="12" font-weight="bold">startLevel — 0 % demand (ramp starts)</text>
<!-- DEAD ZONE band -->
<rect x="300" y="360" width="240" height="80" fill="#FFF8E1" stroke="#F57C00" />
<text x="420" y="390" text-anchor="middle" font-size="13" fill="#B78200" font-weight="bold">DEAD ZONE</text>
<text x="420" y="408" text-anchor="middle" font-size="12" fill="#B78200">hysteresis — keep last cmd</text>
<!-- heightInlet (inside dead zone) -->
<line x1="270" y1="405" x2="290" y2="405" stroke="#1F4E79" stroke-width="2" />
<text x="550" y="409" fill="#1F4E79" font-size="12">heightInlet</text>
<!-- stopLevel -->
<line x1="265" y1="450" x2="295" y2="450" stroke="#6C3483" stroke-width="3" />
<text x="305" y="454" fill="#6C3483" font-size="12" font-weight="bold">stopLevel — unconditional STOP</text>
<!-- STOP band -->
<rect x="300" y="465" width="240" height="80" fill="#F4ECF7" stroke="#6C3483" />
<text x="420" y="500" text-anchor="middle" font-size="13" fill="#6C3483" font-weight="bold">pumps OFF</text>
<text x="420" y="518" text-anchor="middle" font-size="12" fill="#6C3483">(MGC shutdown)</text>
<!-- heightOutlet -->
<line x1="270" y1="540" x2="290" y2="540" stroke="#B22222" stroke-width="2" />
<text x="305" y="544" fill="#B22222" font-size="12">heightOutlet — outflow pipe (dry-run trip)</text>
<!-- floor -->
<line x1="265" y1="600" x2="295" y2="600" stroke="#000" stroke-width="2" />
<text x="305" y="604" font-size="11">0 (floor)</text>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,58 @@
<mxfile host="app.diagrams.net" modified="2026-04-22T12:00:00.000Z" agent="Claude Code placeholder" etag="initial" version="22.0.0" type="device">
<diagram name="safety-rules" id="safetyRules">
<mxGraphModel dx="1200" dy="700" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="900" pageHeight="700" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="title" value="Safety rules — asymmetric by direction" style="text;html=1;fontSize=16;fontStyle=1;align=center;" vertex="1" parent="1">
<mxGeometry x="150" y="20" width="600" height="30" as="geometry" />
</mxCell>
<mxCell id="dryrun_box" value="DRY-RUN&#10;(direction = draining)" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#FFF3E0;strokeColor=#E65100;strokeWidth=2;fontSize=14;fontStyle=1;verticalAlign=top;" vertex="1" parent="1">
<mxGeometry x="80" y="80" width="340" height="340" as="geometry" />
</mxCell>
<mxCell id="dr_upstream" value="upstream children — KEEP" style="text;html=1;fontSize=13;align=left;" vertex="1" parent="1">
<mxGeometry x="100" y="140" width="300" height="24" as="geometry" />
</mxCell>
<mxCell id="dr_downstream" value="downstream children — STOP" style="text;html=1;fontSize=13;align=left;fontStyle=1;fontColor=#E65100;" vertex="1" parent="1">
<mxGeometry x="100" y="170" width="300" height="24" as="geometry" />
</mxCell>
<mxCell id="dr_machinegroups" value="machineGroups — STOP" style="text;html=1;fontSize=13;align=left;fontStyle=1;fontColor=#E65100;" vertex="1" parent="1">
<mxGeometry x="100" y="200" width="300" height="24" as="geometry" />
</mxCell>
<mxCell id="dr_control" value="control loop — BLOCKED" style="text;html=1;fontSize=13;align=left;fontStyle=1;fontColor=#E65100;" vertex="1" parent="1">
<mxGeometry x="100" y="230" width="300" height="24" as="geometry" />
</mxCell>
<mxCell id="dr_note" value="safetyControllerActive = true&#10;&#10;Pumps must stop before sucking air." style="text;html=1;fontSize=12;align=left;fontStyle=2;" vertex="1" parent="1">
<mxGeometry x="100" y="290" width="300" height="80" as="geometry" />
</mxCell>
<mxCell id="overfill_box" value="OVERFILL&#10;(direction = filling)" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#FFEBEE;strokeColor=#C62828;strokeWidth=2;fontSize=14;fontStyle=1;verticalAlign=top;" vertex="1" parent="1">
<mxGeometry x="480" y="80" width="340" height="340" as="geometry" />
</mxCell>
<mxCell id="of_upstream" value="upstream children — STOP ⚠" style="text;html=1;fontSize=13;align=left;fontStyle=1;fontColor=#C62828;" vertex="1" parent="1">
<mxGeometry x="500" y="140" width="300" height="24" as="geometry" />
</mxCell>
<mxCell id="of_downstream" value="downstream children — KEEP" style="text;html=1;fontSize=13;align=left;" vertex="1" parent="1">
<mxGeometry x="500" y="170" width="300" height="24" as="geometry" />
</mxCell>
<mxCell id="of_machinegroups" value="machineGroups — KEEP" style="text;html=1;fontSize=13;align=left;" vertex="1" parent="1">
<mxGeometry x="500" y="200" width="300" height="24" as="geometry" />
</mxCell>
<mxCell id="of_control" value="control loop — ACTIVE" style="text;html=1;fontSize=13;align=left;" vertex="1" parent="1">
<mxGeometry x="500" y="230" width="300" height="24" as="geometry" />
</mxCell>
<mxCell id="of_note" value="Level control keeps commanding downstream MGC.&#10;&#10;⚠ &quot;upstream STOP&quot; is only correct in a cascaded layout. In a gravity-sewer station the inflow can&apos;t be stopped — log the spill instead." style="text;html=1;fontSize=12;align=left;fontStyle=2;" vertex="1" parent="1">
<mxGeometry x="500" y="290" width="300" height="120" as="geometry" />
</mxCell>
<mxCell id="trigger_title" value="Triggers (either condition fires the rule):" style="text;html=1;fontSize=13;fontStyle=1;align=left;" vertex="1" parent="1">
<mxGeometry x="80" y="450" width="740" height="20" as="geometry" />
</mxCell>
<mxCell id="trigger_list" value="• vol &lt; triggerLowVol (triggerLowVol = minVol × (1 + pct/100))&#10;• vol &gt; triggerHighVol (triggerHighVol = maxVolOverflow × pct/100)&#10;• remainingTime &lt; timeleftToFullOrEmptyThresholdSeconds (if enabled)" style="text;html=1;fontSize=12;align=left;" vertex="1" parent="1">
<mxGeometry x="80" y="480" width="740" height="80" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 620" font-family="Arial, sans-serif" font-size="13">
<title>Safety rules — asymmetric by direction</title>
<text x="450" y="30" text-anchor="middle" font-weight="bold" font-size="16">Safety rules — asymmetric by direction</text>
<!-- DRY-RUN box -->
<rect x="80" y="80" width="340" height="340" fill="#FFF3E0" stroke="#E65100" stroke-width="2" />
<text x="250" y="112" text-anchor="middle" font-weight="bold" font-size="14">DRY-RUN</text>
<text x="250" y="130" text-anchor="middle" font-size="13" fill="#6F4A19">(direction = draining)</text>
<text x="100" y="162" font-size="13">upstream children — <tspan font-weight="bold">KEEP</tspan></text>
<text x="100" y="188" font-size="13" fill="#E65100">downstream children — <tspan font-weight="bold">STOP</tspan></text>
<text x="100" y="214" font-size="13" fill="#E65100">machineGroups — <tspan font-weight="bold">STOP</tspan></text>
<text x="100" y="240" font-size="13" fill="#E65100">control loop — <tspan font-weight="bold">BLOCKED</tspan></text>
<line x1="100" y1="268" x2="400" y2="268" stroke="#E65100" stroke-dasharray="3 3" />
<text x="100" y="294" font-size="12" font-style="italic">safetyControllerActive = true</text>
<text x="100" y="316" font-size="12" font-style="italic">Pumps must stop before sucking air.</text>
<!-- OVERFILL box -->
<rect x="480" y="80" width="340" height="340" fill="#FFEBEE" stroke="#C62828" stroke-width="2" />
<text x="650" y="112" text-anchor="middle" font-weight="bold" font-size="14">OVERFILL</text>
<text x="650" y="130" text-anchor="middle" font-size="13" fill="#7A1919">(direction = filling)</text>
<text x="500" y="162" font-size="13" fill="#C62828">upstream children — <tspan font-weight="bold">STOP</tspan></text>
<text x="500" y="188" font-size="13">downstream children — <tspan font-weight="bold">KEEP</tspan></text>
<text x="500" y="214" font-size="13">machineGroups — <tspan font-weight="bold">KEEP</tspan></text>
<text x="500" y="240" font-size="13">control loop — <tspan font-weight="bold">ACTIVE</tspan></text>
<line x1="500" y1="268" x2="800" y2="268" stroke="#C62828" stroke-dasharray="3 3" />
<text x="500" y="294" font-size="12" font-style="italic">Level control keeps commanding downstream MGC.</text>
<text x="500" y="324" font-size="12" font-style="italic" fill="#C62828">⚠ "upstream STOP" is only correct in a cascaded layout.</text>
<text x="500" y="342" font-size="12" font-style="italic" fill="#C62828">In a gravity-sewer station the inflow can't be</text>
<text x="500" y="360" font-size="12" font-style="italic" fill="#C62828">stopped — log the spill instead.</text>
<!-- Triggers block -->
<text x="80" y="470" font-weight="bold" font-size="13">Triggers (either condition fires the rule):</text>
<text x="100" y="498" font-size="12">• vol &lt; triggerLowVol (triggerLowVol = minVol × (1 + pct/100))</text>
<text x="100" y="520" font-size="12">• vol &gt; triggerHighVol (triggerHighVol = maxVolOverflow × pct/100)</text>
<text x="100" y="542" font-size="12">• remainingTime &lt; timeleftToFullOrEmptyThresholdSeconds (if enabled)</text>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,335 @@
---
title: pumpingStation — Functional Description
node: pumpingStation
updated: 2026-04-22
status: draft
---
# pumpingStation — Functional Description
The `pumpingStation` node models an S88 **Process Cell**: a wet-well basin with inflow and outflow, wrapped around one or more pump controllers. Every second it recomputes the basin's water balance, picks the most trustworthy net-flow source, runs its safety interlocks, and finally commands its children (individual pumps, `machineGroupControl`, or nested pumping stations) so the level stays inside the safe operating band.
This page is the operator-facing reference, derived from [`src/specificClass.js`](../src/specificClass.js). For the 3-tier code layout see [EVOLV — Node Architecture](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/main/wiki/architecture/node-architecture.md); for the atomic pump model see the [rotatingMachine wiki](https://gitea.wbd-rd.nl/RnD/rotatingMachine/wiki).
> **Diagrams on this page are editable.** Sources live in [`diagrams/`](diagrams/) — open the `.drawio` file in [draw.io](https://app.diagrams.net/), export to SVG, commit. See [`diagrams/README.md`](diagrams/README.md).
## At a glance
| Item | Value |
|---|---|
| Node category | EVOLV |
| S88 level | Process Cell (`#0c99d9`, lane L5) |
| Inputs | 1 (message-driven) |
| Outputs | 3 — `process` / `dbase` / `parent` |
| Tick period | 1 s |
| Basin model | Rectangular prismatic — `volume = level × surfaceArea` |
| Canonical units (internal) | Pa, m³/s, W, K, m, m³ |
| Control modes implemented | `levelbased`, `manual` (placeholders for `flowbased`, `pressureBased`, `percentageBased`, `powerBased`, `hybrid`) |
| Default flow dead-band | `1e-4 m³/s` (≈ 0.36 m³/h) |
## Lifecycle
1. **Construct.** The node merges the user's editor config over the schema defaults, creates the measurement store, and seeds the predicted volume at the basin's operational floor (`minVol`).
2. **Register children.** Sensors, pumps, machine groups, and nested stations register via the Port-2 handshake. The station subscribes only to the *highest-level aggregator* for predicted flow to avoid double-counting (MGC if present, otherwise the individual pump).
3. **Tick loop (1 s).** `_updatePredictedVolume → _selectBestNetFlow → _safetyController → _controlLogic → state snapshot → output`.
## Editor configuration
Every field on the pumpingStation editor maps directly to the config schema in `generalFunctions/src/configs/pumpingStation.json`.
### Basin geometry (section `basin`)
| Field | Default | Meaning |
|---|---|---|
| **Basin Volume (m³)** | `1` | Total geometric volume of the empty basin (floor to rim). |
| **Basin Height (m)** | `1` | Physical wall height from floor to rim. |
| **Inlet Elevation (m)** | `2` | Centre of the inlet pipe, measured from the floor. |
| **Outlet Elevation (m)** | `0.2` | Centre of the pump-suction pipe, measured from the floor. |
| **Overflow Level (m)** | `2.5` | Overflow-weir crest, measured from the floor. Above this → overfill safety. |
Constant cross-section is assumed: `surfaceArea = volume / height`. All derived volumes (`minVolOut`, `minVolIn`, `maxVolOverflow`, `maxVol`) are computed once in `initBasinProperties()` and kept on `station.basin`.
### Hydraulics (section `hydraulics`)
| Field | Default | Meaning |
|---|---|---|
| **Minimum Height Based On** | `outlet` | `outlet``minVol = heightOutlet × area` (includes the buffer). `inlet``minVol = heightInlet × area` (buffer treated as unavailable). |
| **Reference Height** | `NAP` | Vertical datum: `NAP` / `EVRF` / `EGM2008`. Metadata only — not used in math today. |
| **Basin Bottom (m Refheight)** | `0` | Absolute elevation of the basin floor, for cross-basin comparisons. |
### Control (section `control`)
| Field | Default | Meaning |
|---|---|---|
| **Control mode** | `levelbased` | Active control strategy. Schema enumerates seven modes; today `levelbased` is fully implemented, `manual` forwards demand via `Qd`, others are placeholders. |
| **startLevel (m)** | `1` | At or below this level, the station is in the DEAD ZONE — pumps stay in their last state. |
| **stopLevel (m)** | `1` | Below this level → unconditional MGC shutdown. |
| **Min flow (m)** | `1` | Bottom of the linear scaling range (0 % demand). Should equal `startLevel`. |
| **Max flow (m)** | `4` | Top of the linear scaling range (100 % demand). Typically ≈ `heightOverflow`. |
| **Flow setpoint** | `0` | Flow-based target (m³/h). Placeholder until `flowbased` is wired. |
| **Deadband** | `0` | Flow-based deadband (m³/h). Placeholder. |
### Safety (section `safety`)
| Field | Default | Meaning |
|---|---|---|
| **Time To Empty/Full (s)** | `0` | If > 0, triggers safety when predicted time-to-overflow or time-to-empty falls below this value. `0` disables time-based protection. |
| **Enable Dry-Run Protection** | `true` | If on, pumps are shut down once volume drops below the dry-run threshold while draining. |
| **Low Volume Threshold (%)** | `2` | Dry-run trigger: `triggerLowVol = minVol × (1 + pct/100)`. |
| **Enable Overfill Protection** | `true` | If on, upstream inflows are shut down once volume climbs above the overfill threshold while filling. |
| **High Volume Threshold (%)** | `98` | Overfill trigger: `triggerHighVol = maxVolOverflow × pct/100`. |
### Output formats
- **Process Output** — format for Port 0 (`process` / `json` / `csv`).
- **Database Output** — format for Port 1 (`influxdb` / `json` / `csv`).
> **Tip — always configure every field.** The pumpingStation mixes geometry and control thresholds freely. Leaving `heightOverflow` at the schema default of 2.5 m while sizing the basin for 10 m walls produces nonsensical fill-percentages and spurious safety events. See the [EVOLV flow-layout rules §9](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/main/.claude/rules/node-red-flow-layout.md) for the completeness rule.
## Input topics
All commands enter on the single input port. `msg.topic` selects the handler; `msg.payload` carries the argument.
### `changemode`
```json
{ "topic": "changemode", "payload": "manual" }
```
Switches the active control strategy. The new mode must be in `config.control.allowedModes` — unknown values are rejected with a warning. Typical transitions: `levelbased ⇄ manual` for operator override during maintenance.
### `calibratePredictedVolume`
```json
{ "topic": "calibratePredictedVolume", "payload": 3.4 }
```
Hard-reset the predicted volume time-series to the supplied value (m³). Also rewrites the predicted level (derived from the constant-area geometry) and resets the internal flow-integrator state. Use this when a trustworthy measured level becomes available.
### `calibratePredictedLevel`
```json
{ "topic": "calibratePredictedLevel", "payload": 1.8 }
```
Same as above, but caller supplies a level (m). The predicted volume is recomputed via `volume = level × surfaceArea`.
### `q_in`
```json
{ "topic": "q_in", "payload": 300, "unit": "l/s" }
```
Inject a **manual inflow** into the basin. Registered as a predicted flow under the synthetic child `manual-qin` at position `in`. Useful when no physical inflow sensor is wired but the inflow is known externally (e.g. fed from a sewer model).
### `Qd`
```json
{ "topic": "Qd", "payload": 75 }
```
Forward a manual demand to every child aggregator (MGC first, then any direct pumps). **Only honoured when `config.control.mode === 'manual'`** — in any other mode the command is logged and discarded. Mirrors how `rotatingMachine` gates commands behind its mode field. The interpretation of the number depends on the child's scaling (`absolute` = m³/h, `normalized` = 0100 %).
### `registerChild`
Internal. Child nodes (measurements, rotatingMachines, machineGroupControls, nested pumpingStations) emit this on their Port 2 a few hundred ms after deploy. The station resolves the Node-RED node id back to the source object and registers it via `childRegistrationUtils`.
## Output ports
### Port 0 — process data
Delta-compressed payload (only changed fields per tick). Keys follow the standard 4-segment format `<type>.<variant>.<position>.<childId>` plus a handful of top-level state fields merged in by `getOutput()`:
| Key | Meaning |
|---|---|
| `volume.predicted.atequipment.default` | Running predicted volume from the flow integrator (m³). |
| `volume.measured.atequipment.default` | Volume derived from a `measured` level sensor (m³). |
| `level.predicted.atequipment.default` | Predicted level = `volume / area` (m). |
| `level.measured.<position>.<childId>` | Raw level sensor reading (m). |
| `volumePercent.predicted.atequipment.default` | `(vol - minVol) / (maxVolOverflow - minVol) × 100` (%). |
| `flow.predicted.in.<childId>` | Inflow contribution from a registered child (m³/s internally; editor unit on output). |
| `flow.predicted.out.<childId>` | Outflow contribution from a registered child. |
| `flow.measured.<position>.<childId>` | Flow sensor reading. |
| `netFlowRate.<variant>.atequipment.default` | Net flow used for control (inflow outflow). |
| `direction` | `filling` / `draining` / `steady` / `unknown`. |
| `flowSource` | Which variant drove the current control cycle (`measured`, `predicted`, `level:predicted`, `null`). |
| `timeleft` | Predicted seconds to overflow (while filling) or to dry-run (while draining). |
| `volEmptyBasin`, `heightInlet`, `heightOverflow`, `maxVol`, `maxVolOverflow`, `minVol`, `minVolIn`, `minVolOut`, `minHeightBasedOn` | Echoes of the basin geometry for dashboards. |
| `percControl` | Last demand (0100+ %) forwarded to the machine group during level-based control. |
Consumers must cache and merge deltas — the example dashboard flows include a reusable function node that does exactly this.
### Port 1 — dbase (InfluxDB)
Line-protocol payload for the `telemetry` bucket. Tags stay low-cardinality (station name, asset type); fields carry the numeric state. See [EVOLV — InfluxDB Schema Design](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/main/wiki/concepts/influxdb-schema-design.md).
### Port 2 — parent
`{ topic: "registerChild", payload: <this-node-id>, positionVsParent, distance }` — fired once ~100 ms after deploy so an upstream cascade can discover this station. Nested stations use this to register with an outer `pumpingStation` parent.
## Basin model
The basin is modelled as a rectangular prism with constant cross-section. Everything derives from `volume = level × surfaceArea`.
![Basin model — physical layout with control thresholds](diagrams/basin-model.drawio.svg)
*Editable source: [`diagrams/basin-model.drawio`](diagrams/basin-model.drawio). See [`diagrams/README.md`](diagrams/README.md) for the edit-and-export workflow.*
**Typical ordering** (bottom → top): `stopLevel < heightInlet < startLevel = minFlowLevel < maxFlowLevel ≤ heightOverflow`.
> ⚠️ The comment block in `specificClass.js` currently says `startLevel ≤ heightInlet` (inlet above startLevel). The physical convention is the opposite: pumps start *before* the water reaches the gravity inlet, so `heightInlet < startLevel`. Worth fixing in the code comment next time that file is touched.
**minHeightBasedOn** — which pipe defines `minVol`, the operational floor used for the initial seed, the dry-run trigger, and the 0 % point of the fill percentage:
```
outlet (default): inlet:
● maxVolOverflow ● maxVolOverflow
│ │
● heightInlet ● heightInlet ─── minVol
│ │
● heightOutlet ──── minVol ● heightOutlet
│ │
● floor ● floor
Buffer counts as usable stock. Buffer reserved; 0% fill
starts at the inlet.
```
## Net-flow selection
Every tick, `_selectBestNetFlow()` walks a priority ladder and returns the first net flow that clears the dead-band (`|flow| ≥ flowThreshold`):
```
priority source note
1 ────● measured.flow real sensors on inflow/outflow
2 ────● predicted.flow manual q_in + pump-curve outputs
3 ────● level:measured dL/dt × surfaceArea
4 ────● level:predicted dL/dt of the integrator
5 ────● steady (fallback) warn, return { value: 0, source: null }
```
Both **measured** and **predicted** variants are always computed and stored, regardless of which one drives control. The active source surfaces on Port 0 as `flowSource`, so operators can watch sensor drift (measured diverges from predicted), validate the volume integrator, and diagnose "which source was active when X happened?".
The inflow / outflow alias map is deliberately wide so measurements (`upstream`/`downstream`) and predicted-flow subscriptions (`in`/`out`) both feed the same aggregator:
```js
flowPositions = { inflow: ['in', 'upstream'], outflow: ['out', 'downstream'] }
```
## Control logic
### `levelbased` mode — three zones
![Control zones — level axis with RUN / DEAD ZONE / STOP bands](diagrams/control-zones.drawio.svg)
- **STOP.** Below `stopLevel` every machine group receives `turnOffAllMachines()` and `percControl` is reset to `0`.
- **DEAD ZONE.** Between `stopLevel` and `startLevel` no command is issued. Pumps currently running keep their last setpoint; pumps currently off stay off. This prevents rapid on/off cycling near the threshold.
- **RUN.** Above `startLevel` the linear scaling range `[minFlowLevel … maxFlowLevel]` maps to `[0 % … 100 %]` pump demand. The station forwards the same percentage to every registered machine group via `group.handleInput('parent', percControl)`. Above `maxFlowLevel` the demand exceeds 100 %; the MGC clamps internally.
### `manual` mode
`_controlLogic` is a no-op. Demand is injected externally via the `Qd` topic, which calls `forwardDemandToChildren(demand)` — MGCs get the demand unchanged; direct pumps get `demand / count` each.
### Other modes
`flowbased`, `pressureBased`, `percentageBased`, `powerBased`, `hybrid` are enumerated in the schema and selectable via `changemode`, but today fall through to a placeholder / warning. When implemented they will plug into the same `_controlLogic(direction)` switch.
## Safety controller
`_safetyController` runs **before** `_controlLogic` every tick. Two rules, deliberately asymmetric — *dry-run protects the pumps from running themselves into air*, *overfill protects the basin from spilling*.
![Safety rules — dry-run vs overfill](diagrams/safety-rules.drawio.svg)
During overfill, level-based control naturally commands ≥100 % on the downstream MGC because the level is above `maxFlowLevel`.
> ⚠️ **Known limitation — gravity-sewer context.** The "upstream STOP" action only makes sense in a **cascaded** station layout where the upstream equipment is an EVOLV-controllable pump or station. In a conventional wastewater wet-well the inflow is gravity-fed from the municipal sewer and **cannot be stopped** — attempting to would back up toilets. For that case the correct response to an overfill event is to **measure and log the spill over the weir** (for compliance reporting) and raise an alarm, while keeping downstream pumps at maximum demand. The current code fires `execSequence: shutdown` on upstream children regardless of what they are; that should be gated on "is the upstream actually controllable?" and supplemented with overflow-rate tracking. Tracked as follow-up work.
A missing volume reading is treated as a hard fault: every direct machine is sent `execSequence: shutdown` and `safetyControllerActive` latches. Calibrate predicted volume (`calibratePredictedVolume`) or wire a level measurement to recover.
## Registration — which children count as flow?
`_registerPredictedFlowChild` subscribes only to the *highest-level aggregator* to prevent double-counting.
```
Without MGC: With MGC:
[ PumpingStation ] [ PumpingStation ]
│ │ │ │
│ │ │ [ MGC ]
│ │ │ │ │ │
● ● ● ● ● ●
(each pump subscribed (only MGC is subscribed;
directly) MGC aggregates its pumps)
N flow subscriptions. 1 flow subscription.
Risk: double-count if an Pumps' flow is already
MGC is added later. inside the MGC total.
```
Measurement children register separately via `_registerMeasurementChild` and feed the `measured` variant — they never collide with the predicted-flow subscription. Nested `pumpingStation` children are always subscribed and expose their net flow at the parent's position.
## Node status badge
Updated every second by `_updateNodeStatus` in `nodeClass.js`:
```
⬆️ 42.3% | V=4.57 / 10.80 m³ | net: 180 m³/h | t≈12 min
```
| Symbol | Direction | Badge colour |
|---|---|---|
| ⬆️ | `filling` | blue |
| ⬇️ | `draining` | orange |
| ⏸️ | `steady` | green |
| ❔ | `unknown` / missing measurements | grey |
## Example flow
The canonical end-to-end demo lives in the EVOLV superproject at [`examples/pumpingstation-3pumps-dashboard/`](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/main/examples/pumpingstation-3pumps-dashboard). It wires three `rotatingMachine` pumps beneath an MGC beneath a `pumpingStation`, with the dashboard layout rule set (see the [EVOLV flow-layout rules](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/main/.claude/rules/node-red-flow-layout.md)) — a useful template for any new station.
## Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| `fill %` exceeds 100 % or is negative | Basin geometry inconsistent — e.g. `heightOverflow > heightBasin`, or `heightOutlet > heightInlet`. | Cross-check `0 < heightOutlet < heightInlet < heightOverflow ≤ heightBasin` in the editor. |
| Pumps never start in `levelbased` | Level is stuck in the DEAD ZONE between `stopLevel` and `startLevel`, or `minFlowLevel == maxFlowLevel` so scaling collapses. | Widen the control band: move `startLevel` above `stopLevel` and set `maxFlowLevel ≈ heightOverflow`. |
| "No volume data available to safe guard system; shutting down all machines." in logs | No measured level, predicted volume not calibrated, and no inflow/outflow samples yet. | Issue `calibratePredictedVolume` (or `calibratePredictedLevel`) once at startup, or wire a level sensor. |
| `flowSource: null` and `direction: 'steady'` forever | Every flow / level signal falls inside the dead-band (default `1e-4 m³/s`). | Confirm flows are non-zero, or lower `config.general.flowThreshold` for a small-scale demo. |
| `Qd` ignored | Station is not in `manual` mode. | Send `{ topic: 'changemode', payload: 'manual' }` first, or fall back to level-based control. |
| Pumps keep running during overfill | Intended — overfill safety only stops **upstream** equipment; downstream pumps must drain. | To override, switch to `manual` and set `Qd = 0`, or issue an emergency-stop at the MGC. |
| Predicted volume drifts away from measured | Flow integrator has no reference — flows might have the wrong sign, or `unit` is mis-declared. | Call `calibratePredictedVolume` periodically from a measured level. |
## Running it locally
```bash
git clone --recurse-submodules https://gitea.wbd-rd.nl/RnD/EVOLV.git
cd EVOLV
docker compose up -d
# Node-RED: http://localhost:1880 InfluxDB: :8086 Grafana: :3000
```
Then in Node-RED: **Import ▸ Examples ▸ EVOLV ▸ pumpingStation** (or open `examples/pumpingstation-3pumps-dashboard/flow.json`).
## Testing
```bash
cd nodes/pumpingStation
npm test
```
Unit tests live in `test/specificClass.test.js` — construction, basin derivation, measurement registration, net-flow selection, safety interlocks, and calibration.
## Related
- [rotatingMachine wiki](https://gitea.wbd-rd.nl/RnD/rotatingMachine/wiki) — atomic pump model beneath pumpingStation / MGC.
- [measurement wiki](https://gitea.wbd-rd.nl/RnD/measurement/wiki) — sensor conditioning for inflow, outflow, level, and pressure inputs.
- [machineGroupControl wiki](https://gitea.wbd-rd.nl/RnD/machineGroupControl/wiki) — how MGC coordinates multiple pumps.
- [EVOLV — Node Architecture](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/main/wiki/architecture/node-architecture.md) — the entry → nodeClass → specificClass pattern.
- [EVOLV — Group Optimization](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/main/wiki/architecture/group-optimization.md) — pump-group scheduling theory.
- [EVOLV — flow-layout rules](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/main/.claude/rules/node-red-flow-layout.md) — the lane / group / channel layout rules used by the demo flows.