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>
14 KiB
pumpingStation
Reflects code as of
d2384b1· regenerated<YYYY-MM-DD>vianpm 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 + 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
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
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-runnpm run wiki: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. |
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"]:::unit
sub["pumpingstation<br/>(sub-station)"]:::pc
end
m -->|"<type>.measured.<position>"| route1[_subscribeMeasurement<br/>routes to measurementRouter]
mach -->|flow.predicted.<in or out>| route2[_subscribePredictedFlow<br/>+ flowAggregator]
mgc -->|flow.predicted.<in or out>| route2
sub -->|flow.predicted.<in or out>| 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 |
machinegroup |
Stored in this.machineGroups[id]. |
`flow.predicted.<in |
pumpingstation |
Stored in this.stations[id]. |
`flow.predicted.<in |
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 (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.
| 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 |
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 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 |
0–100 % | dry-run trip |
enableOverfillProtection |
safety.enableOverfillProtection |
true |
bool | overfill safety |
overfillThresholdPercent |
safety.overfillThresholdPercent |
98 |
0–100 % | 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.
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
rotatingMachinedirectly. - 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
machineGroupControlis 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. |