Files
pumpingStation/wiki/Home.md
znetsixe ed22f01932 P9.3 + examples: fresh 3-tier flows + pilot wiki Home.md
examples/ (new — was empty except standalone-demo.js):
  01-Basic.json         14 nodes, inject + dashboard, no parent
  02-Integration.json   32 nodes, 2 tabs, measurement + MGC + 2 pumps,
                        link-out/link-in channels per node-red-flow-layout.md
  03-Dashboard.json     63 nodes, 3 tabs (process + UI + setup),
                        FlowFuse charts + sliders, trend-split pattern
  README.md             load instructions
  tools/build-examples.js  regenerator

All canonical topic names only (set.*, cmd.*, data.*, child.*). No
legacy aliases. Every ui-* widget has x/y. Every chart has the full
mandatory key set from node-red-flow-layout.md §4.

wiki/Home.md (new) — pilot page for the 14-section visual-first template.
Sections 5 (topic-contract) + 9 (data-model) are auto-generated via the
new npm run wiki:* scripts; everything else hand-written following
.claude/refactor/WIKI_TEMPLATE.md.

package.json — added wiki:contract / wiki:datamodel / wiki:all scripts
wired to ../generalFunctions/scripts/wikiGen.js.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:50:45 +02:00

286 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# pumpingStation
> **Reflects code as of `d2384b1` · regenerated `<YYYY-MM-DD>` 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
**pumpingStation** is an S88 Process Cell that owns a wet-well basin and orchestrates the pumps that drain it. It tracks measured + 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.
## 2. Position in the platform
```mermaid
flowchart LR
ps[pumpingStation<br/>Process Cell]:::pc
meas_lvl[measurement<br/>type=level<br/>position=atequipment]:::ctrl
meas_in[measurement<br/>type=flow<br/>position=upstream]:::ctrl
mgc[machineGroupControl<br/>Unit]:::unit
meas_lvl -.data.-> ps
meas_in -.data.-> ps
ps -->|set.demand| mgc
mgc -.evt.flow-predicted.-> ps
mgc -->|child.register| ps
classDef pc fill:#0c99d9,color:#fff
classDef unit fill:#50a8d9,color:#000
classDef ctrl fill:#a9daee,color:#000
```
S88 colours: Process Cell `#0c99d9`, Unit `#50a8d9`, Control Module `#a9daee`. Source of truth: `.claude/rules/node-red-flow-layout.md`.
## 3. Capability matrix
| Capability | Status | Notes |
|---|---|---|
| Predicts basin volume from net flow | ✅ | Integrator seeded from `basin.minVol`; recomputes level. |
| Accepts measured level / volume / pressure | ✅ | Routed via `measurementRouter` on child registration. |
| Level-based control strategy | ✅ | Linear or log ramp between `minLevel` and `maxLevel`. |
| Flow-based control strategy | ✅ | PID against `flowSetpoint`. |
| Manual demand passthrough | ✅ | `set.demand` only honoured in `manual` mode. |
| Dry-run safety interlock | ✅ | Stops downstream pumps when volume < `minVol` while draining. |
| Overfill safety interlock | ✅ | Stops upstream equipment when volume crosses overfill threshold. |
| Cascaded children (sub-stations) | ⚠️ | Accepted via `pumpingstation` softwareType but not exercised in production. |
## 4. Code map
```mermaid
flowchart TB
subgraph nodeRED["nodeClass.js — adapter (BaseNodeAdapter)"]
nc["buildDomainConfig()<br/>static DomainClass, commands<br/>static tickInterval = 1000ms"]
end
subgraph domain["specificClass.js — orchestrator (BaseDomain)"]
sc["PumpingStation.configure()<br/>declares ChildRouter rules<br/>tick() → safety → control"]
end
subgraph concerns["src/ concern modules"]
basin["basin/<br/>BasinGeometry + thresholdValidator"]
measurement["measurement/<br/>flowAggregator + router + calibration"]
control["control/<br/>levelbased / flowbased / manual"]
safety["safety/<br/>SafetyController"]
commands["commands/<br/>topic registry + handlers"]
end
nc --> sc
sc --> basin
sc --> measurement
sc --> control
sc --> safety
nc --> commands
```
| 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/` | 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. |
## 5. Topic contract
> **Auto-generated** from `src/commands/index.js`. Do NOT hand-edit between the markers. Re-run `npm run wiki:contract`.
<!-- BEGIN AUTOGEN: topic-contract -->
| Canonical topic | Aliases | Payload | Effect |
|---|---|---|---|
| `set.mode` | `changemode` | `string` | Replaces the named state value with the supplied payload. |
| `child.register` | `registerChild` | `string` | Parent/child plumbing — registers or unregisters a child node. |
| `cmd.calibrate.volume` | `calibratePredictedVolume` | `any` | Triggers an action / sequence — not idempotent. |
| `cmd.calibrate.level` | `calibratePredictedLevel` | `any` | Triggers an action / sequence — not idempotent. |
| `set.inflow` | `q_in` | `any` | Replaces the named state value with the supplied payload. |
| `set.demand` | `Qd` | `any` | Replaces the named state value with the supplied payload. |
<!-- 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"]:::unit
sub["pumpingstation<br/>(sub-station)"]:::pc
end
m -->|"&lt;type&gt;.measured.&lt;position&gt;"| route1[_subscribeMeasurement<br/>routes to measurementRouter]
mach -->|flow.predicted.&lt;in or out&gt;| route2[_subscribePredictedFlow<br/>+ flowAggregator]
mgc -->|flow.predicted.&lt;in or out&gt;| route2
sub -->|flow.predicted.&lt;in or out&gt;| route2
route1 --> tick[tick]
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)` — registers in MeasurementContainer. | `<type>.measured.<position>` for any type (pressure, level, flow, …). |
| `machine` | Stored in `this.machines[id]`. **Skipped when a machineGroup parent is present** to avoid double-counting. | `flow.predicted.<in|out>` per the child's `positionVsParent`. |
| `machinegroup` | Stored in `this.machineGroups[id]`. | `flow.predicted.<in|out>`. |
| `pumpingstation` | Stored in `this.stations[id]`. | `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 (measured.level / flow.predicted.out)
ps->>ps: ChildRouter dispatches to handler
Note over ps: every 1000 ms (static tickInterval)
ps->>fa: tick() — net flow, ETA, predicted volume
ps->>sf: evaluate({direction, secondsRemaining})
alt safety blocked
sf-->>ps: blocked=true, reason
Note over ctl: skipped this tick
else safety clear
ps->>ctl: dispatch(mode, ctx, controlState)
ctl-->>ps: percControl updated
end
ps->>ps: notifyOutputChanged()
ps->>out: msg{topic, payload (delta-compressed)}
```
## 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"` |
| `flowSource` | null | — | `null` |
| `heightBasin` | number | m | `1` |
| `inflowLevel` | number | m | `2` |
| `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` |
| `overflowLevel` | number | m | `2.5` |
| `percControl` | 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 setpoints: min / start / max]
f5[Safety: dry-run % / overfill %]
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.minLevel<br/>control.levelbased.startLevel<br/>control.levelbased.maxLevel]
c5[safety.dryRunThresholdPercent<br/>safety.overfillThresholdPercent]
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` | `2` | ≥ 0 (m) | threshold validator, control |
| `outflowLevel` | `basin.outflowLevel` | `0.2` | ≥ 0 (m) | dead-volume floor |
| `overflowLevel` | `basin.overflowLevel` | `2.5` | > 0 (m) | overfill safety |
| `controlMode` | `control.mode` | `levelbased` | enum | `control/dispatch` |
| `minLevel` | `control.levelbased.minLevel` | `1` | ≥ 0 (m) | `levelBased.run` |
| `startLevel` | `control.levelbased.startLevel` | `1` | ≥ minLevel | ramp foot |
| `maxLevel` | `control.levelbased.maxLevel` | `4` | ≤ overflowLevel | ramp top |
| `enableDryRunProtection` | `safety.enableDryRunProtection` | `true` | bool | `SafetyController` |
| `dryRunThresholdPercent` | `safety.dryRunThresholdPercent` | `2` | 0100 % | dry-run trip |
| `enableOverfillProtection` | `safety.enableOverfillProtection` | `true` | bool | overfill safety |
| `overfillThresholdPercent` | `safety.overfillThresholdPercent` | `98` | 0100 % | overfill trip |
## 10. State chart
Two orthogonal state vectors: **control mode** (operator-driven) and **safety state** (data-driven). The diagram shows them together — most transitions are independent.
```mermaid
stateDiagram-v2
state ControlMode {
[*] --> none
none --> levelbased: set.mode
levelbased --> flowbased: set.mode
flowbased --> manual: set.mode
manual --> levelbased: set.mode
levelbased --> none: set.mode
}
state SafetyState {
[*] --> nominal
nominal --> dryRun: vol < minVol AND draining
nominal --> overfill: vol > overfillThreshold AND filling
dryRun --> nominal: vol ≥ minVol
overfill --> nominal: vol ≤ overfillThreshold
}
```
While the safety state is `dryRun`, control dispatch is **skipped** entirely. While `overfill`, control still runs (pumps must keep draining) but upstream equipment is shut down.
## 11. Examples
Example flows live under `examples/` in the repo. The structured tier-1/2/3 flows for this node are still in progress; until they land, the standalone simulator demo is the only runnable artefact.
| Tier | File | What it shows | Status |
|---|---|---|---|
| Basic | `examples/01-Basic.flow.json` | Inject + dashboard, single basin, no parent | ⏳ TBD |
| Integration | `examples/02-Integration.flow.json` | pumpingStation + MGC + 2 pumps + measurement children | ⏳ TBD |
| Dashboard | `examples/03-Dashboard.flow.json` | Live FlowFuse charts (level, net flow, ETA) | ⏳ TBD |
| Headless | `examples/standalone-demo.js` | Node.js-only simulator, no Node-RED | ✅ in repo |
## 12. Debug recipes
| Symptom | First thing to check | Where to look |
|---|---|---|
| Status badge stuck on `❔ 0.0%` | Did any volume / level measurement register? Watch Port 2 + first-child event. | Editor debug tap on Port 2 + `_subscribeMeasurement` log line. |
| `direction` always `steady` | Net flow inside `general.flowThreshold` dead-band (default 0.0001 m³/s). | `flowAggregator.deriveDirection`. |
| `set.demand` ignored | Mode isn't `manual`. Check `set.mode` history. | `handlers.setDemand` debug log. |
| Predicted volume drifts off measured | Calibration needed — fire `cmd.calibrate.volume` with a known reading. | `measurement/calibration.js`. |
| Pumps don't stop on dry-run | `safety.enableDryRunProtection` must be `true` AND the orchestrator must see `direction='draining'`. | `SafetyController.evaluate`. |
| Threshold-ordering warnings on startup | `validateThresholdOrdering` printed `inflowLevel < overflowLevel` style violations. | `basin/thresholdValidator.js`. |
> 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 pumpingStation for a **wet-well basin** that needs orchestrated drainage. For a single pump with no basin model, use `rotatingMachine` directly.
- Don't use pumpingStation to schedule a fixed pump rota — its modes are reactive (level / flow / manual). Use an external scheduler if you need a calendar-driven schedule.
- Skip pumpingStation if you don't need predicted volume / time-to-full. A bare `machineGroupControl` is lighter when the upstream basin is modelled elsewhere.
## 14. Known limitations / current issues
| # | Issue | Tracked in |
|---|---|---|
| 1 | Cascaded `pumpingstation` children accepted but not exercised in production — semantics of nested stations are not test-covered. | TBD |
| 2 | `pressureBased`, `percentageBased`, `powerBased`, and `hybrid` are in the config enum but not implemented as control strategies. | `control/index.js` — only `levelbased` / `flowbased` / `manual` dispatched. |
| 3 | Predicted-volume integrator can drift over long horizons without a measured-level calibration source. | `cmd.calibrate.volume` is operator-triggered, not automatic. |
| 4 | Tier 1/2/3 example flows not yet written — current `examples/` only contains the standalone simulator. | P2.14 (Docker E2E) + P9 wiki cleanup. |