# Reference — Architecture ![code-ref](https://img.shields.io/badge/code--ref-b825ac1-blue) > [!NOTE] > Code structure for `pumpingStation`: the three-tier sandwich, the `src/` layout, the FSM, the lifecycle, and the output-port pipeline. Everything here is reproducible from `src/`. For an intuitive overview, return to [Home](Home). --- ## Three-tier code layout ``` nodes/pumpingStation/ | +-- pumpingStation.js entry: RED.nodes.registerType('pumpingstation', NodeClass) | +-- src/ | nodeClass.js extends BaseNodeAdapter (Node-RED bridge) | specificClass.js extends BaseDomain (orchestration only) | | | +-- commands/ | | index.js topic descriptors | | handlers.js pure handler functions | | | +-- basin/ | | BasinGeometry.js basin shape, level <-> volume conversion | | thresholdValidator.js derives + validates safety / control thresholds | | | +-- measurement/ | | flowAggregator.js net-flow + predicted-volume integrator | | measurementRouter.js routes measurement-child events | | calibration.js calibrate-to-known-level / volume helpers | | | +-- control/ | | index.js mode dispatcher (levelbased, manual, ...) | | | +-- safety/ | safetyController.js dry-run + high-volume + panic guards ``` ### Tier responsibilities | Tier | File | What it owns | Touches `RED.*` | |:---|:---|:---|:---:| | entry | `pumpingStation.js` | Type registration | Yes | | nodeClass | `src/nodeClass.js` | Input routing, tick loop, output ports, status badge | Yes | | specificClass | `src/specificClass.js` | Wire concern modules in `configure()`; run them in `tick()`; nothing more | No | The specificClass is stitching, not implementation. All real work lives in `basin/`, `measurement/`, `control/`, `safety/`. --- ## State chart — safety controller The pumpingStation does not have a per-mode FSM (control modes are stateless transfer functions). The state machine that matters is the **safety controller**, which can block or pass control commands. ```mermaid stateDiagram-v2 [*] --> running running --> blocked_dryrun: level < dryRunLevel running --> blocked_highvolume: level >= highVolumeSafetyLevel running --> blocked_panic: no-data panic timer expires blocked_dryrun --> running: level recovers above hysteresis blocked_highvolume --> running: level falls below hysteresis blocked_panic --> running: data resumes ``` Each `blocked_*` state sets `safety.blocked = true` on Port 0 and prevents the control layer from emitting a non-zero demand. The hysteresis is mode-independent and lives in `src/safety/safetyController.js`. ### Safety-rules asymmetry The `dryRunLevel` and `highVolumeSafetyLevel` rules differ in **which children they stop**: ![Dry-run vs high-volume safety asymmetry](diagrams/safety-rules.drawio.svg) | Rule | What stops | Why | |:---|:---|:---| | Dry run | All children (pumps off) | Pumps cavitate without water; protect the equipment | | High volume | Only outflow-side pumps | Spill is the lesser evil; some pumps may still serve safety functions | --- ## Lifecycle — one tick ```mermaid sequenceDiagram autonumber participant tick as 1s tick participant sc as specificClass.tick() participant fa as flowAggregator participant safe as safetyController participant ctrl as control[mode] participant out as Port 0 / 1 tick->>sc: tick() sc->>fa: update predicted volume fa->>fa: pick best net-flow source (measured / aggregated) sc->>safe: evaluate alt safety blocked safe-->>sc: { blocked: true } Note over sc: skip control layer else safe to run sc->>ctrl: strategies[mode].run(context) ctrl-->>sc: demand 0..100 end sc->>out: getOutput() — emit Port 0 + Port 1 deltas ``` Each tick is 1 Hz. The output pipeline (Port 0 + Port 1) is driven by `outputUtils.formatMsg` — only changed fields are sent. --- ## Output ports | Port | Carries | Sample shape | |:---|:---|:---| | 0 (process) | Delta-compressed state snapshot consumed by downstream Node-RED logic | `{topic, payload: {level, volume, demand, direction, safety, etaSeconds}}` | | 1 (telemetry) | InfluxDB line-protocol string with the same fields as Port 0 | `pumpingStation,id=PS1 level=1.62,volume=32.4 ...` | | 2 (register / control) | `child.register` upward at init; internal control plumbing later | `{topic: 'child.register', payload: {ref, softwareType, config}}` | See [EVOLV — Telemetry](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Telemetry) for the full InfluxDB layout. --- ## Tick timing and event sources | Source | Where it fires | What it triggers | |:---|:---|:---| | `setInterval(1000)` | `BaseNodeAdapter` lifecycle | `specificClass.tick()` — the per-second integrator update | | `measurement` emitter event | Child node's `emitter.emit(.measured., ...)` | `measurementRouter` updates the basin balance | | Inbound `msg.topic` | Node-RED input wire | `commandRegistry` dispatch to a handler | | `child.register` from another node | Port 2 of a child | `_subscribeMeasurement` or `_subscribePredictedFlow` | --- ## Where to start reading | If you're changing... | Read first | |:---|:---| | Basin geometry, level/volume conversion | `src/basin/BasinGeometry.js`, `src/basin/thresholdValidator.js` | | Net-flow selection, predicted-volume integration | `src/measurement/flowAggregator.js` | | Calibration commands | `src/measurement/calibration.js` | | Control modes (level-based, manual, future modes) | `src/control/index.js` | | Safety blocks | `src/safety/safetyController.js` | | Topic dispatch | `src/commands/index.js` + `src/commands/handlers.js` | | Adapter, ticking, output ports | `src/nodeClass.js` (and `BaseNodeAdapter` in `generalFunctions`) | --- ## Related pages | Page | Why | |:---|:---| | [Home](Home) | Intuitive overview | | [Reference — Contracts](Reference-Contracts) | Topic + config + child filters | | [Reference — Examples](Reference-Examples) | Shipped example flows | | [Reference — Limitations](Reference-Limitations) | Known limitations and open questions | | [EVOLV — Architecture](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Architecture) | Platform-wide three-tier pattern |