P9.3 + examples: fresh 3-tier flows + pilot wiki Home.md

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>
This commit is contained in:
znetsixe
2026-05-11 14:50:45 +02:00
parent d2384b1a2d
commit ed22f01932
7 changed files with 3688 additions and 1 deletions

285
wiki/Home.md Normal file
View File

@@ -0,0 +1,285 @@
# 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 -->|"&lt;type&gt;.measured.&lt;position&gt;"| route1[_subscribeMeasurement<br/>routes to measurementRouter]
mach -->|flow.predicted.&lt;in or out&gt;| route2[_subscribePredictedFlow<br/>+ flowAggregator]
mgc -->|flow.predicted.&lt;in or out&gt;| route2
sub -->|flow.predicted.&lt;in or out&gt;| 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` | 0100 % | dry-run trip |
| `enableOverfillProtection` | `safety.enableOverfillProtection` | `true` | bool | overfill safety |
| `overfillThresholdPercent` | `safety.overfillThresholdPercent` | `98` | 0100 % | 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. |