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

18 KiB
Raw Blame History

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 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-run npm 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 -->|"&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

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 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.

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.