- Archived 20 pre-refactor pages to wiki/Archive/ with standard banners:
- All 6 architecture/ pages (old _loadConfig/_setupSpecificClass internals,
pre-refactor S88 hierarchy, deployment blueprint)
- All 3 sessions/ logs (Apr-07 + Apr-13 session summaries)
- findings/open-issues-2026-03.md (issues 1-5 all resolved by refactor)
- concepts/generalfunctions-api.md (missing BaseDomain/BaseNodeAdapter)
- concepts/sources-readme.md (empty PDF placeholder, never populated)
- manuals/nodes/rotatingMachine.md + measurement.md (superseded by per-repo wikis)
- Top-level SCHEMA.md, index.md, log.md, metrics.md, overview.md,
knowledge-graph.yaml (all Apr-07 snapshot, pre-refactor)
- Kept wiki/concepts/ domain pages (ASM, PID, pump-affinity, settling, etc.)
- Kept wiki/findings/ proven results (BEP, NCog, curve-non-convexity, stability)
- Kept wiki/manuals/node-red/* (FlowFuse + Node-RED runtime docs, still current)
- Kept wiki/tools/* (utility scripts)
- Updated wiki/Archive.md index with 20 rows
- Fixed wiki/Home.md: Tier 6 was wrongly marked done; corrected to pending;
Tier 9 updated to reflect 2026-05-11 in-progress wave
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
435 lines
15 KiB
Markdown
435 lines
15 KiB
Markdown
---
|
||
title: EVOLV Architecture
|
||
created: 2026-03-01
|
||
updated: 2026-04-07
|
||
status: evolving
|
||
tags: [architecture, node-red, three-layer]
|
||
---
|
||
|
||
> **⚠️ ARCHIVED — pre-refactor (Tier 1–4, 2026-05)**
|
||
>
|
||
> This page describes the architecture before the platform refactor.
|
||
> The current page is the per-node wiki on **[gitea.wbd-rd.nl/RnD](https://gitea.wbd-rd.nl/RnD)** or **[Home](../Home)**.
|
||
>
|
||
> Kept for historical reference only. **Do not update.**
|
||
|
||
|
||
# EVOLV Architecture
|
||
|
||
## 1. System Overview
|
||
|
||
High-level view of how EVOLV fits into the wastewater treatment automation stack.
|
||
|
||
```mermaid
|
||
graph LR
|
||
NR[Node-RED Runtime] <-->|msg objects| EVOLV[EVOLV Nodes]
|
||
EVOLV -->|InfluxDB line protocol| INFLUX[(InfluxDB)]
|
||
INFLUX -->|queries| GRAFANA[Grafana Dashboards]
|
||
EVOLV -->|process output| NR
|
||
EVOLV -->|parent output| NR
|
||
|
||
style NR fill:#b22222,color:#fff
|
||
style EVOLV fill:#0f52a5,color:#fff
|
||
style INFLUX fill:#0c99d9,color:#fff
|
||
style GRAFANA fill:#50a8d9,color:#fff
|
||
```
|
||
|
||
Each EVOLV node produces three outputs:
|
||
| Port | Name | Purpose |
|
||
|------|------|---------|
|
||
| 0 | process | Process data forwarded to downstream nodes |
|
||
| 1 | dbase | InfluxDB-formatted measurement data |
|
||
| 2 | parent | Control messages to parent nodes (e.g. registerChild) |
|
||
|
||
---
|
||
|
||
## 2. Node Architecture (Three-Layer Pattern)
|
||
|
||
Every node follows a consistent three-layer design that separates Node-RED wiring from domain logic.
|
||
|
||
```mermaid
|
||
graph TB
|
||
subgraph "Node-RED Runtime"
|
||
REG["RED.nodes.registerType()"]
|
||
end
|
||
|
||
subgraph "Layer 1 — Wrapper (valve.js)"
|
||
W[wrapper .js]
|
||
W -->|"new nodeClass(config, RED, this, name)"| NC
|
||
W -->|MenuManager| MENU[HTTP /name/menu.js]
|
||
W -->|configManager| CFG[HTTP /name/configData.js]
|
||
end
|
||
|
||
subgraph "Layer 2 — Node Adapter (src/nodeClass.js)"
|
||
NC[nodeClass]
|
||
NC -->|_loadConfig| CFGM[configManager]
|
||
NC -->|_setupSpecificClass| SC
|
||
NC -->|_attachInputHandler| INPUT[onInput routing]
|
||
NC -->|_startTickLoop| TICK[1s tick loop]
|
||
NC -->|_tick → outputUtils| OUT[formatMsg]
|
||
end
|
||
|
||
subgraph "Layer 3 — Domain Logic (src/specificClass.js)"
|
||
SC[specificClass]
|
||
SC -->|measurements| MC[MeasurementContainer]
|
||
SC -->|state machine| ST[state]
|
||
SC -->|hydraulics / biology| DOMAIN[domain models]
|
||
end
|
||
|
||
subgraph "generalFunctions"
|
||
GF[shared library]
|
||
end
|
||
|
||
REG --> W
|
||
GF -.->|logger, outputUtils, configManager,\nMeasurementContainer, validation, ...| NC
|
||
GF -.->|MeasurementContainer, state,\nconvert, predict, ...| SC
|
||
|
||
style W fill:#0f52a5,color:#fff
|
||
style NC fill:#0c99d9,color:#fff
|
||
style SC fill:#50a8d9,color:#fff
|
||
style GF fill:#86bbdd,color:#000
|
||
```
|
||
|
||
---
|
||
|
||
## 3. generalFunctions Module Map
|
||
|
||
The shared library (`nodes/generalFunctions/`) provides all cross-cutting concerns.
|
||
|
||
```mermaid
|
||
graph TB
|
||
GF[generalFunctions/index.js]
|
||
|
||
subgraph "Core Helpers (src/helper/)"
|
||
LOGGER[logger]
|
||
OUTPUT[outputUtils]
|
||
CHILD[childRegistrationUtils]
|
||
CFGUTIL[configUtils]
|
||
ASSERT[assertionUtils]
|
||
VALID[validationUtils]
|
||
end
|
||
|
||
subgraph "Validators (src/helper/validators/)"
|
||
TV[typeValidators]
|
||
CV[collectionValidators]
|
||
CURV[curveValidator]
|
||
end
|
||
|
||
subgraph "Domain Modules (src/)"
|
||
MC[MeasurementContainer]
|
||
CFGMGR[configManager]
|
||
MENUMGR[MenuManager]
|
||
STATE[state]
|
||
CONVERT[convert / Fysics]
|
||
PREDICT[predict / interpolation]
|
||
NRMSE[nrmse / errorMetrics]
|
||
COOLPROP[coolprop]
|
||
end
|
||
|
||
subgraph "Data (datasets/)"
|
||
CURVES[assetData/curves]
|
||
ASSETS[assetData/assetData.json]
|
||
UNITS[unitData.json]
|
||
end
|
||
|
||
subgraph "Constants (src/constants/)"
|
||
POS[POSITIONS / POSITION_VALUES]
|
||
end
|
||
|
||
GF --> LOGGER
|
||
GF --> OUTPUT
|
||
GF --> CHILD
|
||
GF --> CFGUTIL
|
||
GF --> ASSERT
|
||
GF --> VALID
|
||
VALID --> TV
|
||
VALID --> CV
|
||
VALID --> CURV
|
||
GF --> MC
|
||
GF --> CFGMGR
|
||
GF --> MENUMGR
|
||
GF --> STATE
|
||
GF --> CONVERT
|
||
GF --> PREDICT
|
||
GF --> NRMSE
|
||
GF --> COOLPROP
|
||
GF --> CURVES
|
||
GF --> POS
|
||
|
||
style GF fill:#0f52a5,color:#fff
|
||
style LOGGER fill:#86bbdd,color:#000
|
||
style OUTPUT fill:#86bbdd,color:#000
|
||
style VALID fill:#86bbdd,color:#000
|
||
style MC fill:#50a8d9,color:#fff
|
||
style CFGMGR fill:#50a8d9,color:#fff
|
||
style MENUMGR fill:#50a8d9,color:#fff
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Data Flow (Message Lifecycle)
|
||
|
||
Sequence diagram showing a typical input message and the periodic tick output cycle.
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant NR as Node-RED
|
||
participant W as wrapper.js
|
||
participant NC as nodeClass
|
||
participant SC as specificClass
|
||
participant OU as outputUtils
|
||
|
||
Note over W: Node startup
|
||
W->>NC: new nodeClass(config, RED, node, name)
|
||
NC->>NC: _loadConfig (configManager.buildConfig)
|
||
NC->>SC: new specificClass(config, stateConfig, options)
|
||
NC->>NR: send([null, null, {topic: registerChild}])
|
||
|
||
Note over NC: Every 1 second (tick loop)
|
||
NC->>SC: getOutput()
|
||
SC-->>NC: raw measurement data
|
||
NC->>OU: formatMsg(raw, config, 'process')
|
||
NC->>OU: formatMsg(raw, config, 'influxdb')
|
||
NC->>NR: send([processMsg, influxMsg])
|
||
|
||
Note over NR: Incoming control message
|
||
NR->>W: msg {topic: 'execMovement', payload: {...}}
|
||
W->>NC: onInput(msg)
|
||
NC->>SC: handleInput(source, action, setpoint)
|
||
SC->>SC: update state machine & measurements
|
||
```
|
||
|
||
---
|
||
|
||
## 5. Node Types
|
||
|
||
| Node | S88 Level | Purpose |
|
||
|------|-----------|---------|
|
||
| **measurement** | Control Module | Generic measurement point — reads, validates, and stores sensor values |
|
||
| **valve** | Control Module | Valve simulation with hydraulic model, position control, flow/pressure prediction |
|
||
| **rotatingMachine** | Control Module | Pumps, blowers, mixers — rotating equipment with speed control and efficiency curves |
|
||
| **diffuser** | Control Module | Aeration diffuser — models oxygen transfer and pressure drop |
|
||
| **settler** | Equipment | Sludge settler — models settling behavior and sludge blanket |
|
||
| **reactor** | Equipment | Hydraulic tank and biological process simulator (activated sludge, digestion) |
|
||
| **monster** | Equipment | MONitoring and STrEam Routing — complex measurement aggregation |
|
||
| **pumpingStation** | Unit | Coordinates multiple pumps as a pumping station |
|
||
| **valveGroupControl** | Unit | Manages multiple valves as a coordinated group — distributes flow, monitors pressure |
|
||
| **machineGroupControl** | Unit | Group control for rotating machines — load balancing and sequencing |
|
||
| **dashboardAPI** | Utility | Exposes data and unit conversion endpoints for external dashboards |
|
||
# EVOLV Architecture
|
||
|
||
## Node Hierarchy (S88)
|
||
|
||
EVOLV follows the ISA-88 (S88) batch control standard. Each node maps to an S88 level and uses a consistent color scheme in the Node-RED editor.
|
||
|
||
```mermaid
|
||
graph TD
|
||
classDef area fill:#0f52a5,color:#fff,stroke:#0a3d7a
|
||
classDef processCell fill:#0c99d9,color:#fff,stroke:#0977aa
|
||
classDef unit fill:#50a8d9,color:#fff,stroke:#3d89b3
|
||
classDef equipment fill:#86bbdd,color:#000,stroke:#6a9bb8
|
||
classDef controlModule fill:#a9daee,color:#000,stroke:#87b8cc
|
||
classDef standalone fill:#f0f0f0,color:#000,stroke:#999
|
||
|
||
%% S88 Levels
|
||
subgraph "S88: Area"
|
||
PS[pumpingStation]
|
||
end
|
||
|
||
subgraph "S88: Equipment"
|
||
MGC[machineGroupControl]
|
||
VGC[valveGroupControl]
|
||
end
|
||
|
||
subgraph "S88: Control Module"
|
||
RM[rotatingMachine]
|
||
V[valve]
|
||
M[measurement]
|
||
R[reactor]
|
||
S[settler]
|
||
end
|
||
|
||
subgraph "Standalone"
|
||
MON[monster]
|
||
DASH[dashboardAPI]
|
||
DIFF[diffuser - not implemented]
|
||
end
|
||
|
||
%% Parent-child registration relationships
|
||
PS -->|"accepts: measurement"| M
|
||
PS -->|"accepts: machine"| RM
|
||
PS -->|"accepts: machineGroup"| MGC
|
||
PS -->|"accepts: pumpingStation"| PS2[pumpingStation]
|
||
|
||
MGC -->|"accepts: machine"| RM
|
||
|
||
RM -->|"accepts: measurement"| M2[measurement]
|
||
RM -->|"accepts: reactor"| R
|
||
|
||
VGC -->|"accepts: valve"| V
|
||
VGC -->|"accepts: machine / rotatingmachine"| RM2[rotatingMachine]
|
||
VGC -->|"accepts: machinegroup / machinegroupcontrol"| MGC2[machineGroupControl]
|
||
VGC -->|"accepts: pumpingstation / valvegroupcontrol"| PS3["pumpingStation / valveGroupControl"]
|
||
|
||
R -->|"accepts: measurement"| M3[measurement]
|
||
R -->|"accepts: reactor"| R2[reactor]
|
||
|
||
S -->|"accepts: measurement"| M4[measurement]
|
||
S -->|"accepts: reactor"| R3[reactor]
|
||
S -->|"accepts: machine"| RM3[rotatingMachine]
|
||
|
||
%% Styling
|
||
class PS,PS2,PS3 area
|
||
class MGC,MGC2 equipment
|
||
class VGC equipment
|
||
class RM,RM2,RM3 controlModule
|
||
class V controlModule
|
||
class M,M2,M3,M4 controlModule
|
||
class R,R2,R3 controlModule
|
||
class S controlModule
|
||
class MON,DASH,DIFF standalone
|
||
```
|
||
|
||
### Registration Summary
|
||
|
||
```mermaid
|
||
graph LR
|
||
classDef parent fill:#0c99d9,color:#fff
|
||
classDef child fill:#a9daee,color:#000
|
||
|
||
PS[pumpingStation] -->|measurement| LEAF1((leaf))
|
||
PS -->|machine| RM1[rotatingMachine]
|
||
PS -->|machineGroup| MGC1[machineGroupControl]
|
||
PS -->|pumpingStation| PS1[pumpingStation]
|
||
|
||
MGC[machineGroupControl] -->|machine| RM2[rotatingMachine]
|
||
|
||
VGC[valveGroupControl] -->|valve| V1[valve]
|
||
VGC -->|source| SRC["machine, machinegroup,<br/>pumpingstation, valvegroupcontrol"]
|
||
|
||
RM[rotatingMachine] -->|measurement| LEAF2((leaf))
|
||
RM -->|reactor| R1[reactor]
|
||
|
||
R[reactor] -->|measurement| LEAF3((leaf))
|
||
R -->|reactor| R2[reactor]
|
||
|
||
S[settler] -->|measurement| LEAF4((leaf))
|
||
S -->|reactor| R3[reactor]
|
||
S -->|machine| RM3[rotatingMachine]
|
||
|
||
class PS,MGC,VGC,RM,R,S parent
|
||
class LEAF1,LEAF2,LEAF3,LEAF4,RM1,RM2,RM3,MGC1,PS1,V1,SRC,R1,R2,R3 child
|
||
```
|
||
|
||
## Node Types
|
||
|
||
| Node | S88 Level | softwareType | role | Accepts Children | Outputs |
|
||
|------|-----------|-------------|------|-----------------|---------|
|
||
| **pumpingStation** | Area | `pumpingstation` | StationController | measurement, machine (rotatingMachine), machineGroup, pumpingStation | [process, dbase, parent] |
|
||
| **machineGroupControl** | Equipment | `machinegroupcontrol` | GroupController | machine (rotatingMachine) | [process, dbase, parent] |
|
||
| **valveGroupControl** | Equipment | `valvegroupcontrol` | ValveGroupController | valve, machine, rotatingmachine, machinegroup, machinegroupcontrol, pumpingstation, valvegroupcontrol | [process, dbase, parent] |
|
||
| **rotatingMachine** | Control Module | `rotatingmachine` | RotationalDeviceController | measurement, reactor | [process, dbase, parent] |
|
||
| **valve** | Control Module | `valve` | controller | _(leaf node, no children)_ | [process, dbase, parent] |
|
||
| **measurement** | Control Module | `measurement` | Sensor | _(leaf node, no children)_ | [process, dbase, parent] |
|
||
| **reactor** | Control Module | `reactor` | Biological reactor | measurement, reactor (upstream chaining) | [process, dbase, parent] |
|
||
| **settler** | Control Module | `settler` | Secondary settler | measurement, reactor (upstream), machine (return pump) | [process, dbase, parent] |
|
||
| **monster** | Standalone | - | - | dual-parent, standalone | - |
|
||
| **dashboardAPI** | Standalone | - | - | accepts any child (Grafana integration) | - |
|
||
| **diffuser** | Standalone | - | - | _(not implemented)_ | - |
|
||
|
||
## Data Flow
|
||
|
||
### Measurement Data Flow (upstream to downstream)
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant Sensor as measurement (sensor)
|
||
participant Machine as rotatingMachine
|
||
participant Group as machineGroupControl
|
||
participant Station as pumpingStation
|
||
|
||
Note over Sensor: Sensor reads value<br/>(pressure, flow, level, temp)
|
||
|
||
Sensor->>Sensor: measurements.type(t).variant("measured").position(p).value(v)
|
||
Sensor->>Sensor: emitter.emit("type.measured.position", eventData)
|
||
|
||
Sensor->>Machine: Event: "pressure.measured.upstream"
|
||
Machine->>Machine: Store in own MeasurementContainer
|
||
Machine->>Machine: getMeasuredPressure() -> calcFlow() -> calcPower()
|
||
Machine->>Machine: emitter.emit("flow.predicted.downstream", eventData)
|
||
|
||
Machine->>Group: Event: "flow.predicted.downstream"
|
||
Group->>Group: handlePressureChange()
|
||
Group->>Group: Aggregate flows across all machines
|
||
Group->>Group: Calculate group totals and efficiency
|
||
|
||
Machine->>Station: Event: "flow.predicted.downstream"
|
||
Station->>Station: Store predicted flow in/out
|
||
Station->>Station: _updateVolumePrediction()
|
||
Station->>Station: _calcNetFlow(), _calcTimeRemaining()
|
||
```
|
||
|
||
### Control Command Flow (downstream to upstream)
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant Station as pumpingStation
|
||
participant Group as machineGroupControl
|
||
participant Machine as rotatingMachine
|
||
participant Machine2 as rotatingMachine (2)
|
||
|
||
Station->>Group: handleInput("parent", action, param)
|
||
|
||
Group->>Group: Determine scaling strategy
|
||
Group->>Group: Calculate setpoints per machine
|
||
|
||
Group->>Machine: handleInput("parent", "execMovement", setpoint)
|
||
Group->>Machine2: handleInput("parent", "execMovement", setpoint)
|
||
|
||
Machine->>Machine: setpoint() -> state.moveTo(pos)
|
||
Machine->>Machine: updatePosition() -> calcFlow(), calcPower()
|
||
Machine->>Machine: emitter.emit("flow.predicted.downstream")
|
||
|
||
Machine2->>Machine2: setpoint() -> state.moveTo(pos)
|
||
Machine2->>Machine2: updatePosition() -> calcFlow(), calcPower()
|
||
Machine2->>Machine2: emitter.emit("flow.predicted.downstream")
|
||
```
|
||
|
||
### Wastewater Treatment Process Flow
|
||
|
||
```mermaid
|
||
graph LR
|
||
classDef process fill:#50a8d9,color:#fff
|
||
classDef equipment fill:#86bbdd,color:#000
|
||
|
||
PS_IN[pumpingStation<br/>Influent] -->|flow| R1[reactor<br/>Anoxic]
|
||
R1 -->|effluent| R2[reactor<br/>Aerated]
|
||
R2 -->|effluent| SET[settler]
|
||
SET -->|effluent out| PS_OUT[pumpingStation<br/>Effluent]
|
||
SET -->|sludge return| RM_RET[rotatingMachine<br/>Return pump]
|
||
RM_RET -->|recirculation| R1
|
||
|
||
PS_IN --- MGC_IN[machineGroupControl]
|
||
MGC_IN --- RM_IN[rotatingMachine<br/>Influent pumps]
|
||
|
||
class PS_IN,PS_OUT process
|
||
class R1,R2,SET process
|
||
class MGC_IN,RM_IN,RM_RET equipment
|
||
```
|
||
|
||
### Event-Driven Communication Pattern
|
||
|
||
All parent-child communication uses Node.js `EventEmitter`:
|
||
|
||
1. **Registration**: Parent calls `childRegistrationUtils.registerChild(child, position)` which stores the child and calls the parent's `registerChild(child, softwareType)` method.
|
||
2. **Event binding**: The parent's `registerChild()` subscribes to the child's `measurements.emitter` events (e.g., `"flow.predicted.downstream"`).
|
||
3. **Data propagation**: When a child updates a measurement, it emits an event. The parent's listener stores the value in its own `MeasurementContainer` and runs its domain logic.
|
||
4. **Three outputs**: Every node sends data to three Node-RED outputs: `[process, dbase, parent]` -- process data for downstream nodes, InfluxDB for persistence, and parent aggregation data.
|
||
|
||
### Position Convention
|
||
|
||
Children register with a position relative to their parent:
|
||
- `upstream` -- before the parent in the flow direction
|
||
- `downstream` -- after the parent in the flow direction
|
||
- `atEquipment` -- physically located at/on the parent equipment
|