286 lines
14 KiB
Markdown
286 lines
14 KiB
Markdown
|
|
# 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 -->|"<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|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` | 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.
|
|||
|
|
|
|||
|
|
```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. |
|