Reference — Architecture
Note
Code structure for
pumpingStation: the three-tier sandwich, thesrc/layout, the FSM, the lifecycle, and the output-port pipeline. Everything here is reproducible fromsrc/. For an intuitive overview, return to 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.
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:
| 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
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 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(<type>.measured.<position>, ...) |
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 | Intuitive overview |
| Reference — Contracts | Topic + config + child filters |
| Reference — Examples | Shipped example flows |
| Reference — Limitations | Known limitations and open questions |
| EVOLV — Architecture | Platform-wide three-tier pattern |
pumpingStation
Reference
Related