Files
pumpingStation/wiki/Reference-Architecture.md
znetsixe 8507ee4e02 wiki: split per-node Home into Zone A (intuitive) + Reference-* siblings
New standard, pilot pass for pumpingStation. Sets the pattern the other
10 nodes will follow once we sign off on this one.

Zone A (wiki/Home.md, ~180 lines):
- one-sentence opener
- "at a glance" 5-row fact table
- "How it looks in Node-RED" — screenshot placeholder
- "What it models" — embeds the existing basin-model.drawio.svg
- "Try it" — 3-minute demo with curl-load command, click list,
  GIF placeholder
- "Typical wiring" — two placeholder screenshots (standalone +
  integrated), no mermaid (per user direction)
- "The five things you'll send" + sample Port-0 payload table
- "Need more?" footer linking to Reference-* siblings

Zone B (4 sibling pages):
- Reference-Contracts.md  — full topic contract + data model
  (AUTOGEN markers); config schema; child registration filters;
  unit policy
- Reference-Architecture.md — 3-tier code layout; safety FSM
  (stateDiagram-v2); tick lifecycle (sequenceDiagram); output ports
- Reference-Examples.md — 01-Basic / 02-Integration / 03-Dashboard
  walk-through with per-example screenshot + GIF placeholders;
  debug-recipes table
- Reference-Limitations.md — implemented vs schema-only modes;
  basin-shape constraint; net-flow source caveat; alias-removal map

Asset directory placeholders created:
- wiki/_partial-screenshots/pumpingStation/.gitkeep
- wiki/_partial-gifs/pumpingStation/.gitkeep
- wiki/_partial-flows/pumpingStation/.gitkeep

Abandoned per user direction (no longer linked, removed from source):
- wiki/README.md
- wiki/functional-description.md (377 lines retired)
- wiki/modes/*.md (5 files retired)

Diagrams kept in place (wiki/diagrams/*.drawio.svg) — referenced from
Home and Reference-Architecture.

package.json: wiki:contract + wiki:datamodel now target
Reference-Contracts.md instead of Home.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 09:19:48 +02:00

6.3 KiB

Reference — Architecture

code-ref

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.


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:

Dry-run vs high-volume safety asymmetry

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() &mdash; 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)

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