- 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>
18 KiB
pumpingStation
Reflects code as of
530f84a· regenerated2026-05-11vianpm run wiki:allIf 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 for the full behaviour spec.
2. Position in the platform
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
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-runnpm run wiki: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. |
6. Child registration
Mirrors the ChildRouter declarations in specificClass.js → configure().
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
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/.
8. Data model — getOutput()
What lands on Port 0. Built in getOutput(), then delta-compressed by outputUtils.formatMsg.
| 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 |
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
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 |
enableOverfillProtectionandoverfillThresholdPercentare deprecated aliases still accepted bySafetyControllerfor back-compat. UseenableHighVolumeSafetyandhighVolumeSafetyThresholdPercentin new flows. SeeOPEN_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.
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
rotatingMachinedirectly 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
machineGroupControlis 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. |