Files
pumpingStation/wiki/Home.md
znetsixe b825ac1d6d wiki: rewrite Home.md per visual-first 14-section template
- Banner: update hash to 530f84a and date to 2026-05-11
- Section 2: add rotatingMachine to platform diagram; show full child→MGC→PS data flow
- Section 3: add no-data panic capability row; add unimplemented modes row
- Section 7: expand sequence diagram to show all three safety paths (panic / dry-run / overfill)
- Section 9: fix deprecated config keys (enableOverfillProtection → enableHighVolumeSafety,
  overfillThresholdPercent → highVolumeSafetyThresholdPercent); add missing fields
  (levelCurveType, logCurveFactor, enableShiftedRamp, stopLevel, flowSetpoint,
  timeleftToFullOrEmptyThresholdSeconds); call out deprecated aliases in note
- Section 10: add three-state safety FSM with panic branch; add effect table
- Section 11: update examples table — all three tiers now exist in repo
- Section 14: replace stale 'TBD' example-flows entry with deprecated-alias cleanup item

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

334 lines
18 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 `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
**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
```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
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
```
S88 colours: Process Cell `#0c99d9`, Unit `#50a8d9`, Equipment `#86bbdd`, Control Module `#a9daee`. Source of truth: `.claude/rules/node-red-flow-layout.md §10.1`.
## 3. Capability matrix
| 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`. |
## 4. Code map
```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
```
| 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. |
## 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 | 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 -->|"&lt;type&gt;.measured.&lt;position&gt;"| 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` | 0100 % | dry-run trip volume |
| `enableHighVolumeSafety` | `safety.enableHighVolumeSafety` | `true` | bool | `SafetyController._overfillRule` |
| `highVolumeSafetyThresholdPercent` | `safety.highVolumeSafetyThresholdPercent` | `98` | 0100 % | 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`. |