Wiki overhaul: crisp design, no decoration emoji, all archive pages deleted

User feedback this pass:
- "no silly symbols" — emoji decoration removed from all section headers
- "really crisp so humans can read really well through it" — clean tables,
  Mermaid for graphs, gitea alert callouts for warnings, shields badges
  for metadata only
- "links to non existing pages" — all internal links now use flat names
  matching the wiki.git file names (Concept-ASM-Models, Finding-BEP-...,
  Manual-NodeRED-...)
- "I can also still see the old pages" — all 29 Archive-*.md pages
  hard-deleted from wiki.git (the git history preserves them; Archive.md
  documents the removal and explains how to recover)

Master pages refactored:
- Home — accurate platform mermaid (ground-truth from every node's
  configure()), edge-legend table, S88 hex palette table, live-nodes
  table, start-here trio, full concept/finding/manual index, refactor
  status table
- Architecture — ASCII three-tier sandwich, generalFunctions module
  tree + 12-row API contract, output-port mermaid + table, lifecycle
  sequence diagram, commands registry shape, child-router shape,
  who-accepts-what truth table
- Topology-Patterns — 5 verified patterns + worked WWTP example,
  variants column per pattern, anti-patterns block
- Topic-Conventions — six-prefix table + diagram, alias map, payload
  schemas, unit-coercion flowchart + behaviour table, S88 palette,
  measurement key shape ASCII, status badge, HealthStatus, canonical
  units
- Telemetry — three-port table, per-port section with example,
  output-pipeline sequence, InfluxDB schema layout, FlowFuse + Grafana
  wiring, debug recipes
- Getting-Started — 5 numbered steps, prerequisites table, run-mode
  comparison, quick-command reference
- Glossary — sectioned by S88 / runtime / topics / WWTP / hydraulics /
  control / project terms

Concept/finding/manual pages: uniform mini-header banner added (status
badges + reference-page note) without rewriting domain content.
2026-05-11 22:24:29 +02:00
parent 48a2e509c2
commit 27a42eeeb5
62 changed files with 1481 additions and 4956 deletions

@@ -1,170 +1,335 @@
# Architecture # Architecture
> **Reflects code as of `9ab9f6b` · regenerated `2026-05-11`** ![code-ref](https://img.shields.io/badge/code--ref-9ab9f6b-blue)
![source](https://img.shields.io/badge/source-CONTRACTS.md-orange)
How every EVOLV node is structured, and what the shared `generalFunctions` library provides. > [!NOTE]
> Every EVOLV node is a three-tier sandwich: the entry registers the type with Node-RED; `nodeClass` (extends `BaseNodeAdapter`) bridges runtime to domain; `specificClass` (extends `BaseDomain`) holds pure-JS domain logic with zero `RED.*` imports. Everything shared — `BaseDomain`, `BaseNodeAdapter`, `ChildRouter`, the commands registry, `UnitPolicy`, `MeasurementContainer`, `statusBadge`, `HealthStatus`, `LatestWinsGate`, `logger`, `configManager` — lives in `generalFunctions`. Source of truth: `.claude/refactor/CONTRACTS.md`.
## The 3-tier node pattern ---
```mermaid ## The three-tier pattern
flowchart LR
rt["Node-RED runtime"]:::neutral
subgraph node["Custom node (one folder under nodes/)"]
entry["<NodeName>.js<br/>(entry — registers node type with RED)"]:::tier1
nc["src/<NodeName>NodeClass.js<br/>(nodeClass — Node-RED adapter)"]:::tier2
sc["src/<NodeName>SpecificClass.js<br/>(specificClass — pure domain logic)"]:::tier3
end
rt -->|RED.nodes.registerType| entry
entry -->|new| nc
nc -->|new + configure()| sc
classDef neutral fill:#dddddd,color:#000 ```
classDef tier1 fill:#a9daee,color:#000 Node-RED runtime
classDef tier2 fill:#86bbdd,color:#000 |
classDef tier3 fill:#50a8d9,color:#000 +-- entry: nodes/<name>/<NodeName>.js
| RED.nodes.registerType('<nodeName>', NodeClass)
| HTTP admin endpoints (if any)
|
| +-- nodeClass: src/<...>NodeClass.js
| | extends BaseNodeAdapter (generalFunctions)
| | static DomainClass = SpecificClass
| | static commands = [...descriptors]
| | static tickInterval = null | <ms>
| | buildDomainConfig(uiConfig, nodeId)
| |
| | +-- specificClass: src/<...>SpecificClass.js
| | | extends BaseDomain (generalFunctions)
| | | static name = '<softwareType>'
| | | static unitPolicy = UnitPolicy.declare(...)
| | | configure() <- wire routers + concern modules
| | | tick() <- opt-in time-based math
| | | getOutput() <- Port 0 / 1 snapshot
| | | getStatusBadge() <- node.status badge
``` ```
| Tier | Owns | Touches RED.* API? | Tested by | ### Tier responsibilities
|---|---|---|---|
| entry (`<NodeName>.js`) | Type registration, HTTP admin endpoints | yes | smoke tests |
| nodeClass (`src/...NodeClass.js`, extends `BaseNodeAdapter`) | msg routing, tick loop, output port wiring, status badge updates | yes | integration tests |
| specificClass (`src/...SpecificClass.js`, extends `BaseDomain`) | All business logic; emits via `this.emitter`; calls `this.measurements` / `this.router` | **no** — must be free of RED imports | unit tests |
**Rule:** never import Node-RED APIs in the specificClass. The specificClass is unit-testable by `new SpecificClass(config)`. If you find `RED.*` calls outside the entry/nodeClass tiers, that's a bug. | Tier | File path | Extends | Touches `RED.*` | Unit-testable |
|:---|:---|:---|:---|:---|
| entry | `nodes/<name>/<NodeName>.js` | (top-level) | Yes | No (smoke only) |
| nodeClass | `src/<...>NodeClass.js` | `BaseNodeAdapter` | Yes | Integration only |
| specificClass | `src/<...>SpecificClass.js` | `BaseDomain` | No (never) | Yes |
## generalFunctions — what it provides > [!CAUTION]
> The specificClass must never import Node-RED APIs. If you find `RED.*` calls outside the entry or nodeClass tier, that is a bug. Pure-JS in the specificClass is what makes unit tests possible without spinning up a Node-RED runtime.
The `nodes/generalFunctions` submodule is a plain-JS library every node depends on. Public exports (top-level `require('generalFunctions')`): Source: `.claude/refactor/CONTRACTS.md` §2 and §3.
| Export | Role | ---
|---|---|
| `BaseDomain` | Base class for every specificClass. Owns `measurements`, `router`, `emitter`, `logger`, `unitPolicy`. |
| `BaseNodeAdapter` | Base class for every nodeClass. Wires `commandRegistry` to `node.on('input')`, owns tick loop. |
| `ChildRouter` | Declarative child-registration matcher. `router.onRegister(softwareType, handler)`, `router.onMeasurement(...)`. |
| `commandRegistry` | Topic → handler descriptor map. Owns alias resolution + unit coercion. |
| `UnitPolicy` | Per-node canonical + output units. Coerces incoming `msg.unit` to canonical. |
| `MeasurementContainer` | Chainable storage: `type(t).variant(v).position(p).value(x, ts, unit)`. Key shape: `<type>.<variant>.<position>.<childId>`. |
| `statusBadge` | Composer for `node.status({fill,shape,text})` updates. |
| `HealthStatus` | Standardised `{ level: 0..3, flags: [], message, source }` shape. |
| `LatestWinsGate` | Mutex with supersede semantics — keeps only the freshest in-flight call. |
| `logger` | Structured logger (use this; never `console.log`). |
| `configManager` | Loads JSON schemas from `src/configs/<node>.json`. |
| `MenuManager` | Dynamic editor dropdowns (asset lists). |
| `outputUtils` | Delta-compressed Port-0 + InfluxDB-line-protocol Port-1 formatting. |
See [generalFunctions Home →](https://gitea.wbd-rd.nl/RnD/generalFunctions/wiki/Home) for the full 34-row API table. ## generalFunctions &mdash; what the library provides
The `nodes/generalFunctions` submodule is a plain-JS library every node depends on. Public exports from `require('generalFunctions')`:
```
generalFunctions
|
+-- Bases
| BaseDomain extend in specificClass.js
| BaseNodeAdapter extend in nodeClass.js
|
+-- Wiring
| ChildRouter onRegister / onMeasurement / onPrediction
| commandRegistry topic -> descriptor map
|
+-- Data
| UnitPolicy canonical + output unit declaration
| MeasurementContainer chainable type/variant/position/childId store
| convert unit conversion (m3/s <-> m3/h, ...)
|
+-- Concurrency
| LatestWinsGate supersede-semantics mutex
|
+-- Health and status
| statusBadge node.status({fill, shape, text}) composer
| HealthStatus {level, flags, message, source}
|
+-- Utilities
logger structured (never console.log)
configManager loads configs/<name>.json
MenuManager dynamic editor dropdowns
outputUtils delta-compressed Port 0 / 1 formatting
```
### API one-liners
| Export | Contract |
|:---|:---|
| `BaseDomain` | Owns `emitter`, `config`, `logger`, `measurements`, `child`. Calls subclass `configure()` then `_init?()`. |
| `BaseNodeAdapter` | Builds merged config, instantiates `DomainClass`, emits Port-2 register, wires output strategy, wires status loop, wires input dispatcher. |
| `ChildRouter` | `.onRegister(swType, handler)` · `.onMeasurement(swType, filter, handler)` · `.onPrediction(swType, filter, handler)`. |
| `commandRegistry` | Topic + alias map to handler. Coerces `msg.unit` to descriptor `units.default`. Logs one-time deprecation per alias. |
| `UnitPolicy` | `.canonical(t)` · `.output(t)` · `.curve(t)` · `.resolve()` · `.convert()` · `.containerOptions()`. Dual access: method form or frozen property bag (`policy.canonical.flow`). |
| `MeasurementContainer` | `.type(t).variant(v).position(p).value(x, ts, unit)`. Keys: `<type>.<variant>.<position>.<childId>`. |
| `statusBadge` | `.compose([..])` · `.error(msg)` · `.idle(label)`. Returns `{fill, shape, text}`. |
| `HealthStatus` | `{level: 0..3, flags: string[], message: string, source: string \| null}`. Lower level = healthier. |
| `LatestWinsGate` | `.fire(v)` (no-wait) · `.fireAndWait(v)` (per-call result; superseded calls resolve with sentinel `{superseded: true}`). |
| `logger` | `.info` · `.warn` · `.error` · `.debug`. Named after `config.general.name`. |
| `configManager` | `buildConfig(uiConfig, baseConfig)` &mdash; validates, merges, applies defaults. |
| `outputUtils` | `formatMsg(snapshot, 'process' \| 'influxdb')` &mdash; delta-compressed; only changed fields are emitted. |
The full 34-row API surface is on the [generalFunctions wiki Home](https://gitea.wbd-rd.nl/RnD/generalFunctions/wiki/Home) under "API surface".
---
## Output ports ## Output ports
Every EVOLV node emits on three ports: Every node emits on three ports. Source: `.claude/refactor/CONTRACTS.md` §10.
```mermaid ```mermaid
flowchart LR flowchart LR
sc[specificClass]:::tier3 sc["specificClass &mdash; tick() or 'output-changed'"]
p0[(Port 0<br/>process data)]:::p0 ou["outputUtils.formatMsg &mdash; delta-compress"]
p1[(Port 1<br/>InfluxDB line)]:::p1 p0[("Port 0 &mdash; process")]
p2[(Port 2<br/>registration / control)]:::p2 p1[("Port 1 &mdash; InfluxDB line")]
sc --> p0 p2[("Port 2 &mdash; register / control")]
sc --> p1 dl["downstream Node-RED &mdash; dashboards, functions"]
sc --> p2 influx[("InfluxDB")]
parent["parent EVOLV node"]
p0 -.-> dn1[downstream Node-RED nodes<br/>dashboards, function nodes] sc -- getOutput() --> ou
p1 -.-> influx[(InfluxDB)] ou --> p0 --> dl
p2 -.-> parent[parent EVOLV node<br/>via child.register] ou --> p1 --> influx
sc -. child.register .-> p2 --> parent
class sc tier3
class ou tier2
class p0 p0c
class p1 p1c
class p2 p2c
class dl,parent dn
class influx ext
classDef tier3 fill:#50a8d9,color:#000 classDef tier3 fill:#50a8d9,color:#000
classDef p0 fill:#86bbdd classDef tier2 fill:#86bbdd,color:#000
classDef p1 fill:#a9daee classDef p0c fill:#0c99d9,color:#fff
classDef p2 fill:#dddddd classDef p1c fill:#50a8d9,color:#000
classDef p2c fill:#a9daee,color:#000
classDef dn fill:#dddddd,color:#000
classDef ext fill:#fff2cc,color:#000
``` ```
| Port | Carries | Format | Cardinality | | Port | Direction | Carries | When |
|---|---|---|---| |:---|:---|:---|:---|
| **0** Process | Delta-compressed measurement / state snapshot for downstream Node-RED logic. | `msg.payload` = object of changed keys only. | One msg per tick when something changed. | | 0 | out | Process data, formatted via `outputUtils.formatMsg(..., 'process')`. Object containing only keys that changed since last tick. | `'output-changed'` fires on the emitter, or every tick if tick-driven |
| **1** Telemetry | InfluxDB line-protocol strings: `measurement,tag=val field=val ts`. | `msg.payload` = `string` (or array of strings). | One msg per tick when something changed; all numeric outputs. | | 1 | out | InfluxDB line-protocol string via `outputUtils.formatMsg(..., 'influxdb')`. Numeric fields only. | Same trigger as Port 0 |
| **2** Registration / control | `child.register` upward on adapter init; control replies. | `{topic, payload: nodeRef}` | At init time + on demand. | | 2 | out | `child.register` upward at init plus internal control plumbing. | Once on init (after 100ms delay); on demand |
| in | in | Commands by `msg.topic`, dispatched through the `commands/` registry. | Any time another node sends a msg |
See [Telemetry](Telemetry) for the full Port-1 schema and InfluxDB conventions. See [Telemetry](Telemetry) for the line-protocol layout and downstream wiring.
## Topic conventions ---
| Prefix | Direction | Used for | ## Lifecycle &mdash; what BaseNodeAdapter does
|---|---|---|
| `set.` | inbound | Set a configurable value (mode, setpoint). Idempotent. |
| `cmd.` | inbound | Trigger an action (startup, shutdown, calibrate). Has side-effects. |
| `data.` | inbound or outbound | Carries measurement data between child ↔ parent. |
| `evt.` | outbound | Signal that something happened (state change, alarm). |
| `child.` | inbound (on parent) | Child node registers itself with this parent. |
See [Topic-Conventions](Topic-Conventions) for the full list, payload shapes, alias deprecation map. In order, in the constructor. Source: `.claude/refactor/CONTRACTS.md` §2.
## Child registration
When a node is configured with `parent` = some other node's id, on `init()` the nodeClass emits a `child.register` message on Port 2 toward the parent. The parent's `commandRegistry` routes it into `ChildRouter`, which fires the matching `onRegister(softwareType, handler)` declared in `configure()`.
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
participant childNc as Child nodeClass autonumber
participant parentReg as Parent commandRegistry participant rt as Node-RED runtime
participant parentRouter as Parent ChildRouter participant nc as nodeClass (BaseNodeAdapter)
participant parentSc as Parent specificClass participant sc as specificClass (BaseDomain)
participant outs as Output pipeline
childNc->>parentReg: msg{topic: child.register, softwareType, ref} rt->>nc: new nodeClass(uiConfig)
parentReg->>parentRouter: dispatch(child.register, ref) nc->>nc: configManager.buildConfig(uiConfig)
parentRouter->>parentRouter: match softwareType nc->>nc: this.buildDomainConfig(uiConfig)
parentRouter->>parentSc: invoke registered handler nc->>sc: new DomainClass(mergedConfig)
parentSc->>parentSc: store ref, wire emitter.on(...) sc->>sc: configure() &mdash; wire ChildRouter + concerns
sc->>sc: _init?() &mdash; optional post-configure hook
Note over nc: 100ms delay
nc->>nc: emit Port 2 child.register
nc->>outs: subscribe to 'output-changed' OR start tick(N ms)
nc->>nc: start status loop (every 1000ms)
nc->>nc: attach input handler (commands dispatcher)
rt->>nc: msg.topic dispatch -> handler
nc->>sc: handler.call(source, msg, ctx)
sc->>sc: emit 'output-changed' if state shifted
outs->>rt: Port 0 + Port 1 send (delta-compressed)
``` ```
A child is anything the parent's `configure()` declares via `router.onRegister(<softwareType>, handler)`. Examples: ### Two output strategies
| Parent | Accepts children with softwareType | | Strategy | When to pick | What domain does | What adapter does |
|---|---| |:---|:---|:---|:---|
| pumpingStation | `measurement`, `machine`, `machinegroup`, `pumpingstation` | | Event-driven (default) | Domain reacts to incoming events and has no genuinely time-driven math | Fire `this.emitter.emit('output-changed')` when public output state shifts | Subscribes to `'output-changed'`; on each fire, calls `getOutput()` and pushes the delta-compressed msg |
| machineGroupControl | `machine`, `measurement` | | Tick-driven (opt-in) | Domain has time-driven math &mdash; integrators, simulators, time-based thresholds | Implement `tick()`. Fire `'output-changed'` from inside it when output state shifts | Calls `tick()` every `static tickInterval` ms; listens to `'output-changed'` the same way as event-driven |
| valveGroupControl | `valve`, `machine`, `machinegroup`, `pumpingstation`, `valvegroupcontrol` (last 4 as flow sources) |
| reactor | `measurement`, `reactor` | Both strategies funnel into the same `'output-changed'` &rarr; `getOutput()` &rarr; `formatMsg` &rarr; `node.send` pipeline.
| settler | `measurement`, `reactor`, `machine` |
| monster | `measurement` | ---
| diffuser | `measurement` |
| rotatingMachine | `measurement` | ## The commands registry
| valve | `measurement` |
| dashboardAPI | any (used for Grafana provisioning) | Each node has `src/commands/index.js` exporting an array of descriptors. The base adapter builds a `Map<topic | alias, descriptor>` at construction. Dispatch is one lookup. Source: `.claude/refactor/CONTRACTS.md` §4.
```js
module.exports = [
{
topic: 'set.demand',
aliases: ['setDemand', 'Qd'], // legacy names
units: { measure: 'volumeFlowRate', default: 'm3/h' },
payloadSchema: { type: 'number' },
description: 'Operator demand setpoint.',
handler: handlers.setDemand,
},
{
topic: 'cmd.calibrate',
payloadSchema: { type: 'none' }, // trigger-only
description: 'Trigger a one-shot calibration.',
handler: handlers.calibrate,
},
];
```
| Descriptor field | What it does |
|:---|:---|
| `topic` | Canonical name. See [Topic Conventions](Topic-Conventions). |
| `aliases` | Pre-refactor legacy names. First use of each fires a one-time deprecation warning. |
| `units` | `{measure, default}` &mdash; pre-dispatch unit normalisation. Handler always sees `default` unit. |
| `payloadSchema` | `{type: 'string' \| 'number' \| 'boolean' \| 'object' \| 'any' \| 'none'}` &mdash; type-check before handler. |
| `description` | Free-text. Surfaced by `.list()` and `wikiGen` topic-contract autogen. |
| `handler` | `(source, msg, ctx) => ...` &mdash; pure function on the domain. |
---
## Child registration &mdash; declarative routing
The `ChildRouter` declares which child softwareTypes the parent accepts and what to do with each. Source: `.claude/refactor/CONTRACTS.md` §5.
```js
configure() {
this.router
.onRegister('machine', (child) => this.machines[child.id] = child)
.onRegister('measurement', (child) => this._subscribeMeasurement(child))
.onMeasurement('measurement', { type: 'pressure', position: 'upstream' },
(data, child) => this._onPressure('upstream', data));
}
```
```mermaid
sequenceDiagram
autonumber
participant child as Child nodeClass
participant reg as Parent commandRegistry
participant rt as Parent ChildRouter
participant sc as Parent specificClass
child->>reg: msg{topic: child.register, payload: {ref, softwareType}}
reg->>rt: dispatchRegister(child, softwareType)
rt->>rt: match softwareType in onRegister handlers
rt->>sc: invoke handler(child)
sc->>sc: store ref, wire emitter.on(<topic>, ...)
```
### Who accepts what
Verified against each node's `configure()` in source.
| Parent | Accepted softwareTypes | Use |
|:---|:---|:---|
| pumpingStation | `measurement`, `machine`, `machinegroup`, `pumpingstation` | Basin sensors + pumps + groups + cascaded PS |
| machineGroupControl | `machine`, `measurement` | Pumps + pressure sensors |
| valveGroupControl | `valve`, `machine`, `machinegroup`, `pumpingstation`, `valvegroupcontrol` | Valves + four flow-source softwareTypes (peer-level, not S88 children) |
| reactor | `measurement`, `reactor` | Sensors + upstream reactor in a chain |
| settler | `measurement`, `reactor`, `machine` | Sensors + upstream reactor + return pump |
| monster | `measurement` | Flow + quality sensors |
| diffuser | `measurement` | DO + airflow sensors |
| rotatingMachine | `measurement` | Pressure / flow / power sensors |
| valve | `measurement` | Position / pressure sensors |
| dashboardAPI | any softwareType | Triggers Grafana dashboard generation per node |
---
## Where business logic lives ## Where business logic lives
```mermaid Each node's `src/` follows the same shape (concern modules).
flowchart TB
subgraph node["A node's src/ folder"] ```
sc["specificClass.js<br/>orchestration only"] nodes/<name>/
subgraph concerns["Concern subdirs (per-node)"] |
c1[basin/ or curves/ or kinetics/<br/>physics / math] +-- <NodeName>.js entry
c2[state/<br/>FSM transitions] +-- <NodeName>.html editor form
c3[dispatch/ or safety/<br/>action / guard logic] +-- package.json
c4[commands/<br/>topic → handler descriptors] |
c5[io/<br/>output composition] +-- src/
end | <Name>NodeClass.js nodeClass (adapter)
end | <Name>SpecificClass.js specificClass (orchestrator)
sc --> c1 | |
sc --> c2 | +-- commands/
sc --> c3 | | index.js topic descriptors
sc --> c4 | | handlers.js pure handler functions
sc --> c5 | |
| +-- state/ FSM (if stateful)
| +-- <concern1>/ e.g. basin/, kinetics/, curves/
| +-- <concern2>/ e.g. safety/, dispatch/
| |
| +-- io/
| output.js getOutput() composition
| statusBadge.js getStatusBadge()
|
+-- test/
| basic/ · integration/ · edge/
|
+-- examples/
01-Basic.json · 02-Integration.json · 03-Dashboard.json
``` ```
specificClass should be **stitching only** — instantiate concern modules in `configure()`, call them in `tick()` or in router handlers. Concerns are individually testable. > [!IMPORTANT]
> The specificClass is stitching, not implementation. It instantiates concern modules in `configure()` and calls them in `tick()` or in router handlers. Concerns are individually testable; specificClass tests verify wiring, not math. Source: `.claude/refactor/MODULE_SPLIT.md`.
---
## Reading order for newcomers ## Reading order for newcomers
1. `.claude/refactor/CONTRACTS.md` — every API shape this wiki abstracts over. | # | Read | Why |
2. `.claude/refactor/CONVENTIONS.md` — code style, file size, naming. |:---|:---|:---|
3. `.claude/refactor/MODULE_SPLIT.md` — concern layout per node. | 1 | `.claude/refactor/CONTRACTS.md` | Every API shape this page summarises |
4. One node's `wiki/Home.md` (pumpingStation is the most mature pilot — start there). | 2 | `.claude/refactor/CONVENTIONS.md` | Code style, file size, naming, imports, tests |
5. The corresponding `src/` folder, top-down: specificClass → concern modules. | 3 | `.claude/refactor/MODULE_SPLIT.md` | Concern layout per node |
| 4 | [pumpingStation wiki](https://gitea.wbd-rd.nl/RnD/pumpingStation/wiki/Home) | The refactor pilot &mdash; most mature node |
| 5 | The corresponding `src/` folder | Top-down: specificClass &rarr; concern modules &rarr; handlers |
---
## Related pages ## Related pages
- [Topology-Patterns](Topology-Patterns) — typical plant configurations | Page | Why |
- [Topic-Conventions](Topic-Conventions) — naming and units |:---|:---|
- [Telemetry](Telemetry) — Port-1 InfluxDB schema | [Topology Patterns](Topology-Patterns) | See the contracts above in action across a realistic plant |
- [Getting-Started](Getting-Started) — hands-on first run | [Topic Conventions](Topic-Conventions) | Full reference for `set.` / `cmd.` / `data.` / `query.` / `child.` / `evt.` |
| [Telemetry](Telemetry) | Port 0 / 1 / 2 InfluxDB schema details |
| [Getting Started](Getting-Started) | First hands-on with the contracts |

@@ -1,28 +0,0 @@
> **⚠️ ARCHIVED — pre-refactor wiki page**
>
> This page describes the architecture before the platform refactor (Tier 14, 2026-05).
> The current pages are **[Home](Home)**, **[Architecture](Architecture)**, **[Topology-Patterns](Topology-Patterns)**, **[Topic-Conventions](Topic-Conventions)**, and **[Telemetry](Telemetry)**.
>
> Kept for historical reference only. **Do not update.**
---
# Claude API Token Permissions
For a safe **read + assist** role (Claude reads code, you approve and push), Claude needs:
| Permission | Level | Why |
|---|---|---|
| `repository` | Read and Write | Clone, read files, create branches, push commits, open PRs |
| `issue` | Read and Write | Read/create issues, link commits to tickets |
| `notification` | Read | Stay aware of mentions and PR activity |
| `user` | Read | Identify repo ownership and collaborators |
| `misc` | Read | API metadata, labels, milestones |
And leave these at **No Access**:
| Permission | Why |
|---|---|
| `activitypub` | Federation — irrelevant for dev work |
| `organization` | Claude shouldn't manage org membership or teams |
| `package` | Claude shouldn't publish packages autonomously |

@@ -1,48 +0,0 @@
> **⚠️ ARCHIVED — pre-refactor wiki page**
>
> This page describes the architecture before the platform refactor (Tier 14, 2026-05).
> The current pages are **[Home](Home)**, **[Architecture](Architecture)**, **[Topology-Patterns](Topology-Patterns)**, **[Topic-Conventions](Topic-Conventions)**, and **[Telemetry](Telemetry)**.
>
> Kept for historical reference only. **Do not update.**
---
# EVOLV Configuration Model and Tagcodering
The intended long-term configuration authority for EVOLV is the database-backed `tagcodering` model.
## Role Of Tagcodering
`tagcodering` is intended to hold:
- machine information
- asset metadata
- runtime configuration
- the basis for configuration exchange across edge, site, and central layers
## Configuration Direction
```mermaid
flowchart LR
CFG["Tagcodering"] --> API["Config / Integration API"]
API --> SITE["Site Layer"]
SITE --> EDGE["Edge Layer"]
EDGE --> NODES["EVOLV Node-RED Nodes"]
```
## Architectural Principle
Node-RED flows should consume configuration, not silently become the only source of truth for configuration.
That means EVOLV should move toward:
- database-backed machine and asset metadata
- versioned configuration flows
- controlled rollout from central to site to edge
- less duplication between flows and platform data
## Current Status
`tagcodering` already exists partially but still needs more work to behave as the full configuration backbone.
That gap should be treated as platform work, not as a secondary cleanup task.

@@ -1,84 +0,0 @@
> **⚠️ ARCHIVED — pre-refactor wiki page**
>
> This page describes the architecture before the platform refactor (Tier 14, 2026-05).
> The current pages are **[Home](Home)**, **[Architecture](Architecture)**, **[Topology-Patterns](Topology-Patterns)**, **[Topic-Conventions](Topic-Conventions)**, and **[Telemetry](Telemetry)**.
>
> Kept for historical reference only. **Do not update.**
---
# EVOLV Container Topology
This page translates the deployment blueprint into a practical container/service split.
## Current Repository Baseline
Today the repository contains:
- a development stack in `docker-compose.yml`
- a broad central-stack example in `temp/cloud.yml`
Those are useful references, but production should be split by layer.
## Recommended Service Split
### Edge host
```text
edge-host-01
- evolv-edge-nodered
- evolv-edge-influxdb
- optional evolv-edge-grafana
- optional evolv-edge-broker
```
### Site host
```text
site-host-01
- evolv-site-nodered
- evolv-site-influxdb
- evolv-site-grafana
- optional evolv-site-broker
```
### Central host groups
```text
central-ingress
- reverse proxy
- API gateway
- IAM
central-observability
- central InfluxDB
- Grafana
central-engineering
- Gitea
- CI/CD
central-config
- tagcodering-backed config services
```
## Why Split By Layer
- better fault isolation
- easier upgrades
- clearer secret boundaries
- less confusion between OT-adjacent and enterprise services
## Production Guidance
- keep development Node-RED settings separate from production settings
- add healthchecks for every persistent service
- back up every persistent volume
- avoid exposing edge services publicly
- use env files or secret injection, not inline credentials
## Related Pages
- [Deployment Blueprint](Architecture-Deployment-Blueprint)
- [Security and Access Boundaries](Architecture-Security-and-Access-Boundaries)
- [Deployment Controls Checklist](Architecture-Deployment-Controls-Checklist)

@@ -1,136 +0,0 @@
> **⚠️ ARCHIVED — pre-refactor wiki page**
>
> This page describes the architecture before the platform refactor (Tier 14, 2026-05).
> The current pages are **[Home](Home)**, **[Architecture](Architecture)**, **[Topology-Patterns](Topology-Patterns)**, **[Topic-Conventions](Topic-Conventions)**, and **[Telemetry](Telemetry)**.
>
> Kept for historical reference only. **Do not update.**
---
# EVOLV Deployment Blueprint
This page turns the architecture into a concrete deployment model.
## Layered Deployment Model
### Edge
Purpose:
- PLC and field connectivity
- local Node-RED execution
- local InfluxDB for resilience and digital-twin use
Recommended services:
- Node-RED
- InfluxDB
- optional local Grafana
- optional local broker
### Site
Purpose:
- plant-local aggregation
- mediation between edge and central
- local dashboards and diagnostics
Recommended services:
- Site Node-RED / CoreSync
- Site InfluxDB
- Site Grafana
- optional broker
### Central
Purpose:
- API ingress
- IAM and governance
- fleet analytics and dashboards
- source control, CI/CD, and configuration services
Recommended services:
- reverse proxy / ingress
- API gateway
- IAM
- central InfluxDB
- central Grafana
- Gitea
- CI/CD
- `tagcodering`-backed configuration services
## Target Topology
```mermaid
flowchart LR
subgraph EDGE["Edge Host"]
ENR["Node-RED"]
EDB["InfluxDB"]
EGR["Optional Grafana"]
end
subgraph SITE["Site Host"]
SNR["Site Node-RED / CoreSync"]
SDB["Site InfluxDB"]
SGR["Site Grafana"]
end
subgraph CENTRAL["Central Platform"]
RP["Reverse Proxy / Ingress"]
API["API Gateway"]
IAM["IAM"]
CDB["Central InfluxDB"]
CGR["Grafana"]
GIT["Gitea"]
CICD["CI/CD"]
CFG["Tagcodering Services"]
end
ENR --> EDB
ENR <--> SNR
EDB <--> SDB
SNR --> SGR
SNR <--> API
RP --> API
API --> IAM
API <--> CFG
SDB <--> CDB
CDB --> CGR
GIT --> CICD
```
## Compose Strategy
Do not use one flat compose file for all layers in production.
Preferred split:
- `compose.edge.yml`
- `compose.site.yml`
- `compose.central.yml`
This gives clearer ownership, easier secret separation, and safer updates.
## Environment Strategy
- tracked compose files contain variables only
- real values live in server-local `.env` files or a secret store
- env files should be separated by layer and environment
## Rollout Order
1. edge baseline
2. site mediation
3. central platform
4. `tagcodering` integration
5. smart telemetry policy
## Related Pages
- [Deployment Controls Checklist](Architecture-Deployment-Controls-Checklist)
- [Platform Overview](Architecture-Platform-Overview)
- [Configuration Model and Tagcodering](Architecture-Configuration-Model-and-Tagcodering)

@@ -1,76 +0,0 @@
> **⚠️ ARCHIVED — pre-refactor wiki page**
>
> This page describes the architecture before the platform refactor (Tier 14, 2026-05).
> The current pages are **[Home](Home)**, **[Architecture](Architecture)**, **[Topology-Patterns](Topology-Patterns)**, **[Topic-Conventions](Topic-Conventions)**, and **[Telemetry](Telemetry)**.
>
> Kept for historical reference only. **Do not update.**
---
# EVOLV Deployment Controls Checklist
This page translates the EVOLV architecture into deployment controls that can be checked during implementation, review, FAT/SAT, and audit preparation.
It is not a legal compliance certificate. It is the operational checklist that helps turn the architecture into demonstrable control.
## How To Use This Page
For each deployment:
- mark whether the control is implemented
- record the evidence location
- record the responsible owner
- track open gaps before production release
## Control Checklist
| Control Area | Required Control | Why It Matters | Evidence To Keep |
|---|---|---|---|
| Network segmentation | Edge, site, and central layers are separated by defined network boundaries. | Supports OT/IT separation and reduces blast radius. | Network diagrams, firewall rules, VLAN or conduit definitions. |
| No direct field exposure | PLCs and field-edge runtimes are not exposed as public or enterprise-facing endpoints. | Protects lower operational layers from unnecessary attack surface. | Access matrix, ingress configuration, API gateway design. |
| Central ingress only | External APIs terminate centrally and are mediated before any downward action. | Enforces policy, logging, and safer integration boundaries. | API gateway config, reverse-proxy config, architecture review notes. |
| IAM and access control | Human and service access is authenticated and role-scoped. | Required for controlled operations and traceability. | IAM configuration, role matrix, account review records. |
| Secret handling | Secrets are not stored in tracked manifests; env files or secret-injection mechanisms are used. | Prevents credential leakage and supports safer rotation. | Secret inventory, compose/env config, rotation procedure. |
| Patch and update process | There is a documented process for platform, OS, Node-RED, and dependency updates. | Required for lifecycle security and operational hygiene. | Patch policy, maintenance log, release records. |
| Release traceability | Runtime deployments map to versioned source, commits, and release artifacts. | Supports auditability and controlled rollback. | Git commit references, build artifacts, deployment records. |
| Local resilience | Local Node-RED and local InfluxDB remain usable during central outage. | Supports essential-service continuity and digital-twin/local monitoring use cases. | Outage test results, failover procedure, site acceptance records. |
| Site resilience | Site layer can continue plant-local operation when central connectivity is lost. | Avoids central single-point dependence. | Network-loss test evidence, continuity plan, site operating procedure. |
| Backup and recovery | Backups exist for config, telemetry where required, and critical platform state. | Supports recovery and resilience obligations. | Backup policy, restore test logs, retention schedule. |
| Logging and audit | Security-relevant and operationally critical actions are logged with retention rules. | Supports incident response, review, and accountability. | Logging policy, sample logs, SIEM or retention config. |
| Incident response | Operators know how to detect, escalate, contain, and recover from incidents. | Needed to translate architecture into operational resilience. | Incident runbook, contact tree, exercise records. |
| Data retention | Retention is defined for raw, reconstructed, and summarized telemetry by signal class. | Prevents ambiguity around smart storage and regulatory evidence. | Retention policy, telemetry class matrix, Influx retention config. |
| Reconstruction policy | Smart-storage signals have explicit fidelity and reconstruction rules. | Prevents opaque analytics and compliance disputes. | Signal policy document, reconstruction thresholds, validation results. |
| Config authority | `tagcodering` or approved configuration services are identified as the source of truth. | Prevents uncontrolled config drift across flows and sites. | Config ownership record, schema documentation, API contract. |
| Change governance | Config and runtime changes require approval, versioning, and rollback path. | Reduces unsafe ad hoc plant changes. | Change tickets, deployment approvals, rollback notes. |
| Site commissioning evidence | FAT/SAT and commissioning checks include security and resilience controls, not only functionality. | Confirms architecture is implemented, not just described. | FAT/SAT reports, commissioning checklist, defect log. |
## Minimum Pre-Go-Live Gate
Before a site goes live, the minimum gate should confirm:
- segmentation is implemented
- field assets are not directly exposed
- central ingress and IAM are configured
- local/site outage behavior is tested
- secret handling is clean
- backup and restore have been tested
- retention and smart-storage policy are documented
- configuration authority is identified
## Suggested Status Model
Use one of these statuses per control:
- `implemented`
- `partially implemented`
- `planned`
- `not started`
- `not applicable`
## Related Pages
- [Platform Overview](Architecture-Platform-Overview)
- [Security and Access Boundaries](Architecture-Security-and-Access-Boundaries)
- [Security and Regulatory Mapping](Architecture-Security-and-Regulatory-Mapping)
- [Telemetry and Smart Storage](Architecture-Telemetry-and-Smart-Storage)
- [Configuration Model and Tagcodering](Architecture-Configuration-Model-and-Tagcodering)

@@ -1,82 +0,0 @@
> **⚠️ ARCHIVED — pre-refactor wiki page**
>
> This page describes the architecture before the platform refactor (Tier 14, 2026-05).
> The current pages are **[Home](Home)**, **[Architecture](Architecture)**, **[Topology-Patterns](Topology-Patterns)**, **[Topic-Conventions](Topic-Conventions)**, and **[Telemetry](Telemetry)**.
>
> Kept for historical reference only. **Do not update.**
---
# EVOLV Platform Overview
EVOLV is a layered automation platform:
- edge for plant-side execution
- site for local aggregation and operational resilience
- central for APIs, analytics, advisory intelligence, and governance
## Topology
```mermaid
flowchart LR
subgraph OT["OT / Field"]
PLC["PLC / IO"]
DEV["Sensors / Machines"]
end
subgraph EDGE["Edge Layer"]
ENR["Edge Node-RED"]
EDB["Local InfluxDB"]
EUI["Local Monitoring / Local Dashboards"]
EBR["Optional Local Broker"]
end
subgraph SITE["Site Layer"]
SNR["Site Node-RED / CoreSync"]
SDB["Site InfluxDB"]
SUI["Site Dashboards / SCADA Support"]
SBR["Site Broker"]
end
subgraph CENTRAL["Central Layer"]
API["API / Integration Gateway"]
INTEL["Overview Intelligence / Advisory Logic"]
CDB["Central InfluxDB"]
CGR["Central Grafana"]
CFG["Tagcodering"]
GIT["Gitea + CI/CD"]
IAM["IAM"]
end
DEV --> PLC
PLC --> ENR
ENR --> EDB
ENR --> EUI
ENR --> EBR
ENR <--> SNR
EDB <--> SDB
SNR --> SDB
SNR --> SUI
SNR --> SBR
SNR <--> API
API --> INTEL
API <--> CFG
SDB <--> CDB
CDB --> CGR
IAM --> API
GIT --> ENR
GIT --> SNR
```
## Key Decisions
- Local InfluxDB is required for operational resilience.
- Central is the advisory and API-entry layer, not the direct field caller.
- `tagcodering` is the intended database-backed configuration authority.
- EVOLV should be designed as a platform, not only as a collection of Node-RED nodes.
## Why This Structure
- Edge remains useful and safe during central outages.
- Site absorbs plant-specific complexity and protects field systems.
- Central provides fleet-level visibility, governance, and integration.

@@ -1,130 +0,0 @@
> **⚠️ ARCHIVED — pre-refactor wiki page**
>
> This page describes the architecture before the platform refactor (Tier 14, 2026-05).
> The current pages are **[Home](Home)**, **[Architecture](Architecture)**, **[Topology-Patterns](Topology-Patterns)**, **[Topic-Conventions](Topic-Conventions)**, and **[Telemetry](Telemetry)**.
>
> Kept for historical reference only. **Do not update.**
---
# EVOLV Security and Access Boundaries
EVOLV should expose central services, not field-edge systems, to external consumers.
## Access Boundary
```mermaid
flowchart TD
EXT["External APIs / Enterprise Requests"] --> API["Central API Gateway"]
API --> AUTH["AuthN/AuthZ / Policy Checks"]
AUTH --> INTEL["Central Advisory / Decision Support"]
INTEL --> SITE["Site Integration Layer"]
SITE --> EDGE["Edge Runtime"]
EDGE --> PLC["PLC / Field Assets"]
EXT -. no direct access .-> EDGE
EXT -. no direct access .-> PLC
```
## Principles
- External integrations terminate centrally.
- Central authenticates, authorizes, and mediates requests.
- Site and edge layers receive bounded requests, advice, or setpoints.
- Edge remains protected behind intermediate layers.
## Purdue-Aligned Segmentation
EVOLV is designed to fit a Purdue-style industrial segmentation model:
- field and PLC assets remain at the lower operational layers
- edge runtimes sit close to the process and are not exposed as public integration surfaces
- site systems mediate plant-local services and isolate OT from broader enterprise traffic
- central services host APIs, identity, analytics, and engineering functions above the plant boundary
This matters because it gives a clear answer to external reviewers:
- EVOLV does not require direct enterprise-to-field access
- EVOLV can preserve layered trust boundaries between PLCs, edge runtimes, site services, and central platforms
- EVOLV can support zone/conduit style security designs instead of flattening OT and IT into one network
## European Regulatory Positioning
EVOLV should be described as an architecture that can support compliance with strict European requirements, not as a system that is automatically compliant by default.
### NIS2
Directive (EU) 2022/2555 (NIS2) requires cybersecurity risk-management measures and incident handling for important and essential entities.
This architecture supports that direction by:
- separating edge, site, and central responsibilities
- reducing direct exposure of operational assets
- allowing central policy, identity, logging, and oversight
- preserving local operation during upstream disruption
### CER
Directive (EU) 2022/2557 (Critical Entities Resilience Directive) focuses on resilience of essential services.
This architecture supports CER-style resilience expectations by:
- keeping local and site operation viable during central outages
- enabling business-continuity and degraded-mode operation
- supporting multiple operational layers instead of one central dependency
### Cyber Resilience Act
Regulation (EU) 2024/2847 (Cyber Resilience Act) sets horizontal cybersecurity requirements for products with digital elements.
Where EVOLV components are packaged, distributed, and maintained as digital products, the platform direction supports CRA-aligned engineering by:
- avoiding hard-coded secrets in tracked manifests
- treating updates, identity, security boundaries, and lifecycle management as platform concerns
- supporting traceable releases through central source control and CI/CD
### GDPR
Regulation (EU) 2016/679 (GDPR) applies where EVOLV processes personal data.
The architecture helps support GDPR obligations by:
- centralizing external API ingress
- limiting unnecessary spread of sensitive data toward field layers
- enabling clearer control over access, logging, and retention boundaries
## What We Can Defensibly Claim
The safe and accurate claim is:
- EVOLV can be deployed in a way that aligns with strict European cybersecurity and resilience expectations
- EVOLV can support Purdue-style layered OT/IT segmentation
- EVOLV can be engineered to satisfy strong governance, audit, and access-control requirements
The claim that should be avoided is:
- "EVOLV is automatically compliant"
Actual compliance still depends on:
- configuration
- site implementation
- monitoring and incident response
- supplier and patch management
- data-classification and retention policy
- documented operational controls
## Why This Matters
- lower exposure of OT assets
- better separation between enterprise and plant networks
- easier enforcement of policy, logging, and audit controls
- safer long-term path for overview intelligence and API ecosystems
## Operational Constraint
Central guidance should remain advisory-first. Local site and edge layers must still behave safely when central is degraded or unavailable.
## Related Page
- [Security and Regulatory Mapping](Architecture-Security-and-Regulatory-Mapping)

@@ -1,65 +0,0 @@
> **⚠️ ARCHIVED — pre-refactor wiki page**
>
> This page describes the architecture before the platform refactor (Tier 14, 2026-05).
> The current pages are **[Home](Home)**, **[Architecture](Architecture)**, **[Topology-Patterns](Topology-Patterns)**, **[Topic-Conventions](Topic-Conventions)**, and **[Telemetry](Telemetry)**.
>
> Kept for historical reference only. **Do not update.**
---
# EVOLV Security and Regulatory Mapping
This page maps major security and resilience requirement areas to the EVOLV architecture direction.
It is intended to help explain to reviewers how EVOLV can be deployed to satisfy strict requirements, while staying explicit that compliance depends on implementation and operations as well as architecture.
## Primary Sources
- NIS2: Directive (EU) 2022/2555
- CER: Directive (EU) 2022/2557
- Cyber Resilience Act: Regulation (EU) 2024/2847
- GDPR: Regulation (EU) 2016/679
Official texts:
- https://eur-lex.europa.eu/eli/dir/2022/2555/oj
- https://eur-lex.europa.eu/eli/dir/2022/2557/oj
- https://eur-lex.europa.eu/eli/reg/2024/2847/oj
- https://eur-lex.europa.eu/eli/reg/2016/679/oj
## Mapping Table
| Requirement Area | Regulatory Context | EVOLV Architecture Response | Current Position |
|---|---|---|---|
| OT/IT segmentation | NIS2, CER, Purdue-aligned industry practice | External integrations terminate centrally; field edge is not the public integration surface; site and edge remain behind mediated layers. | Architecture defined, deployment enforcement depends on implementation. |
| Resilience during central outage | CER, NIS2 | Local and site layers retain Node-RED execution and local InfluxDB access so operation and monitoring can continue during central disruption. | Architecture defined, per-site operating procedures still required. |
| Controlled external access | NIS2, GDPR | Central API gateway acts as the entry point; central auth and policy checks mediate requests downward. | Architecture defined, gateway and IAM rollout maturity may vary by deployment. |
| Identity and authorization | NIS2, GDPR, CRA | Central IAM and policy enforcement are treated as platform services rather than ad hoc runtime behavior at the edge. | Target-state architecture; implementation maturity not yet uniform. |
| Secure configuration handling | NIS2, CRA | `tagcodering` is the intended configuration backbone; tracked manifests should use environment variables or secret injection rather than hard-coded credentials. | Direction established; config backbone and secret-handling rollout still in progress. |
| Vulnerability and update management | CRA, NIS2 | Central source control, CI/CD, and release traceability support controlled updates and evidence of change. | Core tooling exists in architecture direction; operating process must be maintained. |
| Logging and auditability | NIS2, GDPR, CER | Central ingress and layered mediation make it easier to define logging and audit boundaries; local/site layers can retain operational evidence. | Partly architectural, partly operational; needs explicit logging policy per deployment. |
| Data minimization and boundary control | GDPR | Centralized ingress and layered architecture reduce unnecessary spread of sensitive data into lower operational layers. | Architecture supports this; actual data classification and retention rules remain deployment-specific. |
| Product cybersecurity lifecycle | CRA | EVOLV treats release, patching, security boundaries, and configuration ownership as platform concerns rather than one-off flow edits. | Direction established; lifecycle evidence depends on maintained process. |
| Business continuity / degraded mode | CER, NIS2 | Edge-first and site-mediated runtime design avoids full dependency on one central runtime for essential service continuity. | Strong architecture fit; continuity plans and tests still required. |
## What Reviewers Can Be Told
The safe statement is:
- EVOLV is architected so it can be deployed in alignment with strict European cybersecurity and resilience expectations.
The unsafe statement is:
- EVOLV is automatically compliant by architecture alone.
## Evidence Expectations
To move from architecture alignment to demonstrable compliance, deployments should be able to show:
- network segmentation and access-control design
- IAM and authorization configuration
- patch and vulnerability-management process
- logging and incident-response process
- backup and recovery approach
- data-classification and retention policy
- evidence of how local, site, and central layers behave during outages

@@ -1,59 +0,0 @@
> **⚠️ ARCHIVED — pre-refactor wiki page**
>
> This page describes the architecture before the platform refactor (Tier 14, 2026-05).
> The current pages are **[Home](Home)**, **[Architecture](Architecture)**, **[Topology-Patterns](Topology-Patterns)**, **[Topic-Conventions](Topic-Conventions)**, and **[Telemetry](Telemetry)**.
>
> Kept for historical reference only. **Do not update.**
---
# EVOLV Telemetry and Smart Storage
EVOLV uses InfluxDB on multiple levels:
- local for resilience and digital-twin use
- site for plant diagnostics and continuity
- central for fleet analytics and advisory intelligence
## Multi-Level Telemetry Flow
```mermaid
flowchart LR
RAW["Raw Signal"] --> EVAL["Edge Signal Evaluation"]
EVAL --> KEEP["Keep critical change points"]
EVAL --> REDUCE["Reduce reconstructable flat spans"]
KEEP --> L0["Local InfluxDB"]
REDUCE --> L0
L0 --> L1["Site InfluxDB"]
L1 --> L2["Central InfluxDB"]
L2 --> DASH["Dashboards / Analytics / Intelligence"]
```
## Design Intent
The target is not only naive event storage with a fixed deadband such as 1%.
The intended model is signal-aware:
- evaluate slope and change behavior
- keep points that carry meaningful process information
- avoid storing large runs of low-information points
- preserve enough context that downstream reconstruction remains auditable
## Benefits
- lower storage volume without throwing away useful process behavior
- better local resilience because important state transitions are preserved
- stronger support for digital-twin and analytics use cases
- more useful site and fleet history
## Risks To Manage
- reconstruction rules must be explicit
- acceptable reconstruction error must be defined per signal class
- compliance-critical signals may need stricter raw retention
- the authoritative layer for each time horizon must be clear
## Direction
Smart storage should be treated as a first-class EVOLV architecture feature, not as an afterthought on top of InfluxDB.

@@ -1,96 +0,0 @@
> **⚠️ ARCHIVED — pre-refactor (Tier 14, 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.**
# Project Wiki Schema
## Purpose
LLM-maintained knowledge base for this project. The LLM writes and maintains everything. You read it (ideally in Obsidian). Knowledge compounds across sessions instead of being lost in chat history.
## Directory Structure
```
wiki/
SCHEMA.md — this file (how to maintain the wiki)
index.md — catalog of all pages with one-line summaries
log.md — chronological record of updates
overview.md — project overview and current status
metrics.md — all numbers with provenance
knowledge-graph.yaml — structured data, machine-queryable
tools/ — search, lint, query scripts
concepts/ — core ideas and mechanisms
architecture/ — design decisions, system internals
findings/ — honest results (what worked AND what didn't)
sessions/ — per-session summaries
```
## Page Conventions
### Frontmatter
Every page starts with YAML frontmatter:
```yaml
---
title: Page Title
created: YYYY-MM-DD
updated: YYYY-MM-DD
status: proven | disproven | evolving | speculative
tags: [tag1, tag2]
sources: [path/to/file.py, commit abc1234]
---
```
### Status values
- **proven**: tested and verified with evidence
- **disproven**: tested and honestly shown NOT to work (document WHY)
- **evolving**: partially working, boundary not fully mapped
- **speculative**: proposed but not yet tested
### Cross-references
Use `[[Page Name]]` Obsidian-style wikilinks.
### Contradictions
When new evidence contradicts a prior claim, DON'T delete the old claim. Add:
```
> [!warning] Superseded
> This was shown to be incorrect on YYYY-MM-DD. See [[New Finding]].
```
### Honesty rule
If something doesn't work, say so. If a result was a false positive, document how it was discovered. The wiki must be trustworthy.
## Operations
### Ingest (after a session or new source)
1. Read outputs, commits, findings
2. Update relevant pages
3. Create new pages for new concepts
4. Update `index.md`, `log.md`, `knowledge-graph.yaml`
5. Check for contradictions with existing pages
### Query
1. Use `python3 wiki/tools/query.py` for structured lookup
2. Use `wiki/tools/search.sh` for full-text
3. Read `index.md` to find relevant pages
4. File valuable answers back into the wiki
### Lint (periodically)
```bash
bash wiki/tools/lint.sh
```
Checks: orphan pages, broken wikilinks, missing frontmatter, index completeness.
## Data Layer
- `knowledge-graph.yaml` — structured YAML with every metric and data point
- `metrics.md` — human-readable dashboard
- When adding new results, update BOTH the wiki page AND the knowledge graph
- The knowledge graph is the single source of truth for numbers
## Source of Truth Hierarchy
1. **Test results** (actual outputs) — highest authority
2. **Code** (current state) — second authority
3. **Knowledge graph** (knowledge-graph.yaml) — structured metrics
4. **Wiki pages** — synthesis, may lag
5. **Chat/memory** — ephemeral, may be stale

@@ -1,64 +0,0 @@
---
title: 3D Pump Curve Architecture
created: 2026-04-07
updated: 2026-04-07
status: proven
tags: [predict, curves, interpolation, rotatingMachine]
sources: [nodes/generalFunctions/src/predict/predict_class.js, nodes/rotatingMachine/src/specificClass.js]
---
> **⚠️ ARCHIVED — pre-refactor (Tier 14, 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.**
# 3D Pump Curve Prediction
## Data Structure
A family of 2D curves indexed by pressure (f-dimension):
- **X-axis**: control position (0-100%)
- **Y-axis**: flow (nq) or power (np) in canonical units
- **F-dimension**: pressure (Pa) — the 3rd dimension
Raw curves are in curve units (m3/h, kW, mbar). `_normalizeMachineCurve()` converts to canonical (m3/s, W, Pa).
## Interpolation
Monotonic cubic spline (Fritsch-Carlson) in both dimensions:
- **X-Y splines**: at each discrete pressure level
- **F-splines**: across pressure levels for intermediate pressure interpolation
## Prediction Flow
```
predict.y(x):
1. Clamp x to [currentFxyXMin, currentFxyXMax]
2. Normalize x to [normMin, normMax]
3. Evaluate spline at normalized x for current fDimension
4. Return y in canonical units (m3/s or W)
```
## Unit Conversion Chain
```
Raw curve (m3/h, kW, mbar)
→ _normalizeMachineCurve → canonical (m3/s, W, Pa)
→ predict class → canonical output
→ MeasurementContainer.getCurrentValue(outputUnit) → output units
```
No double-conversion. Clean separation: specificClass handles units, predict handles normalization/interpolation.
## Three Predict Instances per Machine
- `predictFlow`: control % → flow (nq curve)
- `predictPower`: control % → power (np curve)
- `predictCtrl`: flow → control % (reversed nq curve)
## Boundary Behavior
- Below/above curve X range: flat extrapolation (clamped)
- Below/above f-dimension range: clamped to min/max pressure level
## Performance
- `y(x)`: O(log n), effectively O(1) for 5-10 data points
- `buildAllFxyCurves`: sub-10ms for typical curves
- Full caching of normalized curves, splines, and calculated curves

@@ -1,286 +0,0 @@
---
title: EVOLV Deployment Blueprint
created: 2026-03-01
updated: 2026-04-07
status: evolving
tags: [deployment, docker, edge, site, central]
---
> **⚠️ ARCHIVED — pre-refactor (Tier 14, 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 Deployment Blueprint
## Purpose
This document turns the current EVOLV architecture into a concrete deployment model.
It focuses on:
- target infrastructure layout
- container/service topology
- environment and secret boundaries
- rollout order from edge to site to central
It is the local source document behind the wiki deployment pages.
## 1. Deployment Principles
- edge-first operation: plant logic must continue when central is unavailable
- site mediation: site services protect field systems and absorb plant-specific complexity
- central governance: external APIs, analytics, IAM, CI/CD, and shared dashboards terminate centrally
- layered telemetry: InfluxDB exists where operationally justified at edge, site, and central
- configuration authority: `tagcodering` should become the source of truth for configuration
- secrets hygiene: tracked manifests contain variables only; secrets live in server-side env or secret stores
## 2. Layered Deployment Model
### 2.1 Edge node
Purpose:
- interface with PLCs and field assets
- execute local Node-RED logic
- retain local telemetry for resilience and digital-twin use cases
Recommended services:
- `evolv-edge-nodered`
- `evolv-edge-influxdb`
- optional `evolv-edge-grafana`
- optional `evolv-edge-broker`
Should not host:
- public API ingress
- central IAM
- source control or CI/CD
### 2.2 Site node
Purpose:
- aggregate one or more edge nodes
- host plant-local dashboards and engineering visibility
- mediate traffic between edge and central
Recommended services:
- `evolv-site-nodered` or `coresync-site`
- `evolv-site-influxdb`
- `evolv-site-grafana`
- optional `evolv-site-broker`
### 2.3 Central platform
Purpose:
- fleet-wide analytics
- API and integration ingress
- engineering lifecycle and releases
- identity and governance
Recommended services:
- reverse proxy / ingress
- API gateway
- IAM
- central InfluxDB
- central Grafana
- Gitea
- CI/CD runner/controller
- optional broker for asynchronous site/central workflows
- configuration services over `tagcodering`
## 3. Target Container Topology
### 3.1 Edge host
Minimum viable edge stack:
```text
edge-host-01
- Node-RED
- InfluxDB
- optional Grafana
```
Preferred production edge stack:
```text
edge-host-01
- Node-RED
- InfluxDB
- local health/export service
- optional local broker
- optional local dashboard service
```
### 3.2 Site host
Minimum viable site stack:
```text
site-host-01
- Site Node-RED / CoreSync
- Site InfluxDB
- Site Grafana
```
Preferred production site stack:
```text
site-host-01
- Site Node-RED / CoreSync
- Site InfluxDB
- Site Grafana
- API relay / sync service
- optional site broker
```
### 3.3 Central host group
Central should not be one giant undifferentiated host forever. It should trend toward at least these responsibility groups:
```text
central-ingress
- reverse proxy
- API gateway
- IAM
central-observability
- central InfluxDB
- Grafana
central-engineering
- Gitea
- CI/CD
- deployment orchestration
central-config
- tagcodering-backed config services
```
For early rollout these may be colocated, but the responsibility split should remain clear.
## 4. Compose Strategy
The current repository shows:
- `docker-compose.yml` as a development stack
- `temp/cloud.yml` as a broad central-stack example
For production, EVOLV should not rely on one flat compose file for every layer.
Recommended split:
- `compose.edge.yml`
- `compose.site.yml`
- `compose.central.yml`
- optional overlay files for site-specific differences
Benefits:
- clearer ownership per layer
- smaller blast radius during updates
- easier secret and env separation
- easier rollout per site
## 5. Environment And Secrets Strategy
### 5.1 Current baseline
`temp/cloud.yml` now uses environment variables instead of inline credentials. That is the minimum acceptable baseline.
### 5.2 Recommended production rule
- tracked compose files contain `${VARIABLE}` placeholders only
- real secrets live in server-local `.env` files or a managed secret store
- no shared default production passwords in git
- separate env files per layer and per environment
Suggested structure:
```text
/opt/evolv/
compose.edge.yml
compose.site.yml
compose.central.yml
env/
edge.env
site.env
central.env
```
## 6. Recommended Network Flow
### 6.1 Northbound
- edge publishes or syncs upward to site
- site aggregates and forwards selected data to central
- central exposes APIs and dashboards to approved consumers
### 6.2 Southbound
- central issues advice, approved config, or mediated requests
- site validates and relays to edge where appropriate
- edge remains the execution point near PLCs
### 6.3 Forbidden direct path
- enterprise or internet clients should not directly query PLC-connected edge runtimes
## 7. Rollout Order
### Phase 1: Edge baseline
- deploy edge Node-RED
- deploy local InfluxDB
- validate PLC connectivity
- validate local telemetry and resilience
### Phase 2: Site mediation
- deploy site Node-RED / CoreSync
- connect one or more edge nodes
- validate site-local dashboards and outage behavior
### Phase 3: Central services
- deploy ingress, IAM, API, Grafana, central InfluxDB
- deploy Gitea and CI/CD services
- validate controlled northbound access
### Phase 4: Configuration backbone
- connect runtime layers to `tagcodering`
- reduce config duplication in flows
- formalize config promotion and rollback
### Phase 5: Smart telemetry policy
- classify signals
- define reconstruction rules
- define authoritative layer per horizon
- validate analytics and auditability
## 8. Immediate Technical Recommendations
- treat `docker/settings.js` as development-only and create hardened production settings separately
- split deployment manifests by layer
- define env files per layer and environment
- formalize healthchecks and backup procedures for every persistent service
- define whether broker usage is required at edge, site, central, or only selectively
## 9. Next Technical Work Items
1. create draft `compose.edge.yml`, `compose.site.yml`, and `compose.central.yml`
2. define server directory layout and env-file conventions
3. define production Node-RED settings profile
4. define site-to-central sync path
5. define deployment and rollback runbook

@@ -1,53 +0,0 @@
---
title: Group Optimization Architecture
created: 2026-04-07
updated: 2026-04-07
status: proven
tags: [machineGroupControl, optimization, BEP-Gravitation]
sources: [nodes/machineGroupControl/src/specificClass.js]
---
> **⚠️ ARCHIVED — pre-refactor (Tier 14, 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.**
# machineGroupControl Optimization
## Algorithm: BEP-Gravitation + Marginal-Cost Refinement
### Step 1 — Pressure Equalization
Sets all non-operational pumps to the group's max downstream / min upstream pressure. Ensures fair curve evaluation across combinations.
### Step 2 — Combination Enumeration
Generates all 2^n pump subsets (n = number of machines). Filters by:
- Machine state (excludes off, cooling, stopping, emergency)
- Mode compatibility (`execsequence` allowed in auto)
- Flow bounds: `sumMinFlow ≤ Qd ≤ sumMaxFlow`
- Optional power cap
### Step 3 — BEP-Gravitation Distribution (per combination)
1. **BEP seed**: `estimatedBEP = minFlow + span * NCog` per pump
2. **Slope estimation**: samples dP/dQ at BEP ± delta (directional: slopeLeft, slopeRight)
3. **Slope redistribution**: iteratively shifts flow from steep to flat curves (weight = 1/slope)
4. **Marginal-cost refinement**: after slope redistribution, shifts flow from highest actual dP/dQ to lowest using real `inputFlowCalcPower` evaluations. Converges regardless of curve convexity. Max 50 iterations, typically 5-15.
### Step 4 — Best Selection
Pick combination with lowest total power. Tiebreak by deviation from BEP.
### Step 5 — Execution
Start/stop pumps as needed, send `flowmovement` commands in output units via `_canonicalToOutputFlow()`.
## Three Control Modes
| Mode | Distribution | Combination Selection |
|------|-------------|----------------------|
| optimalControl | BEP-Gravitation + refinement | exhaustive 2^n |
| priorityControl | equal split, priority-ordered | sequential add/remove |
| priorityPercentageControl | percentage-based, normalized | count-based |
## Key Design Decision
The `flowmovement` command sends flow in the **machine's output units** (m3/h), not canonical (m3/s). The `_canonicalToOutputFlow()` helper converts before sending. Without this conversion, every pump stays at minimum flow (the critical bug fixed on 2026-04-07).

@@ -1,434 +0,0 @@
---
title: EVOLV Architecture
created: 2026-03-01
updated: 2026-04-07
status: evolving
tags: [architecture, node-red, three-layer]
---
> **⚠️ ARCHIVED — pre-refactor (Tier 14, 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

@@ -1,166 +0,0 @@
---
title: EVOLV Platform Architecture
created: 2026-03-01
updated: 2026-04-07
status: evolving
tags: [architecture, platform, edge-first]
---
> **⚠️ ARCHIVED — pre-refactor (Tier 14, 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 Platform Architecture
## At A Glance
EVOLV is not only a Node-RED package. It is a layered automation platform:
- edge for plant-side execution
- site for local aggregation and resilience
- central for coordination, analytics, APIs, and governance
```mermaid
flowchart LR
subgraph EDGE["Edge"]
PLC["PLC / IO"]
ENR["Node-RED"]
EDB["Local InfluxDB"]
EUI["Local Monitoring"]
end
subgraph SITE["Site"]
SNR["CoreSync / Site Node-RED"]
SDB["Site InfluxDB"]
SUI["Site Dashboards"]
end
subgraph CENTRAL["Central"]
API["API Gateway"]
CFG["Tagcodering"]
CDB["Central InfluxDB"]
CGR["Grafana"]
INTEL["Overview Intelligence"]
GIT["Gitea + CI/CD"]
end
PLC --> ENR
ENR --> EDB
ENR --> EUI
ENR <--> SNR
EDB <--> SDB
SNR --> SUI
SNR <--> API
API <--> CFG
API --> INTEL
SDB <--> CDB
CDB --> CGR
GIT --> ENR
GIT --> SNR
```
## Core Principles
### 1. Edge-first operation
The edge layer must remain useful and safe when central systems are down.
That means:
- local logic remains operational
- local telemetry remains queryable
- local dashboards can keep working
### 2. Multi-level telemetry
InfluxDB is expected on multiple levels:
- local for resilience and digital-twin use
- site for plant diagnostics
- central for fleet analytics and advisory logic
### 3. Smart storage
Telemetry should not be stored only with naive deadband rules.
The target model is signal-aware:
- preserve critical change points
- reduce low-information flat sections
- allow downstream reconstruction where justified
```mermaid
flowchart LR
SIG["Process Signal"] --> EVAL["Slope / Event Evaluation"]
EVAL --> KEEP["Keep critical points"]
EVAL --> REDUCE["Reduce reconstructable points"]
KEEP --> L0["Local InfluxDB"]
REDUCE --> L0
L0 --> L1["Site InfluxDB"]
L1 --> L2["Central InfluxDB"]
```
### 4. Central is the safe entry point
External systems should enter through central APIs, not by directly calling field-edge systems.
```mermaid
flowchart TD
EXT["External Request"] --> API["Central API Gateway"]
API --> AUTH["Auth / Policy"]
AUTH --> SITE["Site Layer"]
SITE --> EDGE["Edge Layer"]
EDGE --> PLC["Field Assets"]
EXT -. blocked .-> EDGE
EXT -. blocked .-> PLC
```
### 5. Configuration belongs in `tagcodering`
The intended configuration source of truth is the database-backed `tagcodering` model:
- machine metadata
- asset configuration
- runtime-consumable configuration
- future central/site configuration services
This already exists partially but still needs more work before it fully serves that role.
## Layer Roles
### Edge
- PLC connectivity
- local logic
- protocol translation
- local telemetry buffering
- local monitoring and digital-twin support
### Site
- aggregation of edge systems
- local dashboards and diagnostics
- mediation between OT and central
- protected handoff for central requests
### Central
- enterprise/API gateway
- fleet dashboards
- analytics and intelligence
- source control and CI/CD
- configuration governance through `tagcodering`
## Why This Matters
This architecture gives EVOLV:
- better resilience
- safer external integration
- better data quality for analytics
- a path from Node-RED package to platform

@@ -1,640 +0,0 @@
---
title: EVOLV Architecture Review
created: 2026-03-01
updated: 2026-04-07
status: evolving
tags: [architecture, stack, review]
---
> **⚠️ ARCHIVED — pre-refactor (Tier 14, 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 Review
## Purpose
This document captures:
- the architecture implemented in this repository today
- the broader edge/site/central architecture shown in the drawings under `temp/`
- the key strengths and weaknesses of that direction
- the currently preferred target stack based on owner decisions from this review
It is the local staging document for a later wiki update.
## Evidence Used
Implemented stack evidence:
- `docker-compose.yml`
- `docker/settings.js`
- `docker/grafana/provisioning/datasources/influxdb.yaml`
- `package.json`
- `nodes/*`
Target-state evidence:
- `temp/fullStack.pdf`
- `temp/edge.pdf`
- `temp/CoreSync.drawio.pdf`
- `temp/cloud.yml`
Owner decisions from this review:
- local InfluxDB is required for operational resilience
- central acts as the advisory/intelligence and API-entry layer, not as a direct field caller
- intended configuration authority is the database-backed `tagcodering` model
- architecture wiki pages should be visual, not text-only
## 1. What Exists Today
### 1.1 Product/runtime layer
The codebase is currently a modular Node-RED package for wastewater/process automation:
- EVOLV ships custom Node-RED nodes for plant assets and process logic
- nodes emit both process/control messages and telemetry-oriented outputs
- shared helper logic lives in `nodes/generalFunctions/`
- Grafana-facing integration exists through `dashboardAPI` and Influx-oriented outputs
### 1.2 Implemented development stack
The concrete development stack in this repository is:
- Node-RED
- InfluxDB 2.x
- Grafana
That gives a clear local flow:
1. EVOLV logic runs in Node-RED.
2. Telemetry is emitted in a time-series-oriented shape.
3. InfluxDB stores the telemetry.
4. Grafana renders operational dashboards.
### 1.3 Existing runtime pattern in the nodes
A recurring EVOLV pattern is:
- output 0: process/control message
- output 1: Influx/telemetry message
- output 2: registration/control plumbing where relevant
So even in its current implemented form, EVOLV is not only a Node-RED project. It is already a control-plus-observability platform, with Node-RED as orchestration/runtime and InfluxDB/Grafana as telemetry and visualization services.
## 2. What The Drawings Describe
Across `temp/fullStack.pdf` and `temp/CoreSync.drawio.pdf`, the intended platform is broader and layered.
### 2.1 Edge / OT layer
The drawings consistently place these capabilities at the edge:
- PLC / OPC UA connectivity
- Node-RED container as protocol translator and logic runtime
- local broker in some variants
- local InfluxDB / Prometheus style storage in some variants
- local Grafana/SCADA in some variants
This is the plant-side operational layer.
### 2.2 Site / local server layer
The CoreSync drawings also show a site aggregation layer:
- RWZI-local server
- Node-RED / CoreSync services
- site-local broker
- site-local database
- upward API-based synchronization
This layer decouples field assets from central services and absorbs plant-specific complexity.
### 2.3 Central / cloud layer
The broader stack drawings and `temp/cloud.yml` show a central platform layer with:
- Gitea
- Jenkins
- reverse proxy / ingress
- Grafana
- InfluxDB
- Node-RED
- RabbitMQ / messaging
- VPN / tunnel concepts
- Keycloak in the drawing
- Portainer in the drawing
This is a platform-services layer, not just an application runtime.
## 3. Architecture Decisions From This Review
These decisions now shape the preferred EVOLV target architecture.
### 3.1 Local telemetry is mandatory for resilience
Local InfluxDB is not optional. It is required so that:
- operations continue when central SCADA or central services are down
- local dashboards and advanced digital-twin workflows can still consume recent and relevant process history
- local edge/site layers can make smarter decisions without depending on round-trips to central
### 3.2 Multi-level InfluxDB is part of the architecture
InfluxDB should exist on multiple levels where it adds operational value:
- edge/local for resilience and near-real-time replay
- site for plant-level history, diagnostics, and resilience
- central for fleet-wide analytics, benchmarking, and advisory intelligence
This is not just copy-paste storage at each level. The design intent is event-driven and selective.
### 3.3 Storage should be smart, not only deadband-driven
The target is not simple "store every point" or only a fixed deadband rule such as 1%.
The desired storage approach is:
- observe signal slope and change behavior
- preserve points where state is changing materially
- store fewer points where the signal can be reconstructed downstream with sufficient fidelity
- carry enough metadata or conventions so reconstruction quality is auditable
This implies EVOLV should evolve toward smart storage and signal-aware retention rather than naive event dumping.
### 3.4 Central is the intelligence and API-entry layer
Central may advise and coordinate edge/site layers, but external API requests should not hit field-edge systems directly.
The intended pattern is:
- external and enterprise integrations terminate centrally
- central evaluates, aggregates, authorizes, and advises
- site/edge layers receive mediated requests, policies, or setpoints
- field-edge remains protected behind an intermediate layer
This aligns with the stated security direction.
### 3.5 Configuration source of truth should be database-backed
The intended configuration authority is the database-backed `tagcodering` model, which already exists but is not yet complete enough to serve as the fully realized source of truth.
That means the architecture should assume:
- asset and machine metadata belong in `tagcodering`
- Node-RED flows should consume configuration rather than silently becoming the only configuration store
- more work is still needed before this behaves as the intended central configuration backbone
## 4. Visual Model
### 4.1 Platform topology
```mermaid
flowchart LR
subgraph OT["OT / Field"]
PLC["PLC / IO"]
DEV["Sensors / Machines"]
end
subgraph EDGE["Edge Layer"]
ENR["Edge Node-RED"]
EDB["Local InfluxDB"]
EUI["Local Grafana / Local Monitoring"]
EBR["Optional Local Broker"]
end
subgraph SITE["Site Layer"]
SNR["Site Node-RED / CoreSync"]
SDB["Site InfluxDB"]
SUI["Site Grafana / SCADA Support"]
SBR["Site Broker"]
end
subgraph CENTRAL["Central Layer"]
API["API / Integration Gateway"]
INTEL["Overview Intelligence / Advisory Logic"]
CDB["Central InfluxDB"]
CGR["Central Grafana"]
CFG["Tagcodering Config Model"]
GIT["Gitea"]
CI["CI/CD"]
IAM["IAM / Keycloak"]
end
DEV --> PLC
PLC --> ENR
ENR --> EDB
ENR --> EUI
ENR --> EBR
ENR <--> SNR
EDB <--> SDB
SNR --> SDB
SNR --> SUI
SNR --> SBR
SNR <--> API
API --> INTEL
API <--> CFG
SDB <--> CDB
INTEL --> SNR
CGR --> CDB
CI --> GIT
IAM --> API
IAM --> CGR
```
### 4.2 Command and access boundary
```mermaid
flowchart TD
EXT["External APIs / Enterprise Requests"] --> API["Central API Gateway"]
API --> AUTH["AuthN/AuthZ / Policy Checks"]
AUTH --> INTEL["Central Advisory / Decision Support"]
INTEL --> SITE["Site Integration Layer"]
SITE --> EDGE["Edge Runtime"]
EDGE --> PLC["PLC / Field Assets"]
EXT -. no direct access .-> EDGE
EXT -. no direct access .-> PLC
```
### 4.3 Smart telemetry flow
```mermaid
flowchart LR
RAW["Raw Signal"] --> EDGELOGIC["Edge Signal Evaluation"]
EDGELOGIC --> KEEP["Keep Critical Change Points"]
EDGELOGIC --> SKIP["Skip Reconstructable Flat Points"]
EDGELOGIC --> LOCAL["Local InfluxDB"]
LOCAL --> SITE["Site InfluxDB"]
SITE --> CENTRAL["Central InfluxDB"]
KEEP --> LOCAL
SKIP -. reconstruction assumptions / metadata .-> SITE
CENTRAL --> DASH["Fleet Dashboards / Analytics"]
```
## 5. Upsides Of This Direction
### 5.1 Strong separation between control and observability
Node-RED for runtime/orchestration and InfluxDB/Grafana for telemetry is still the right structural split:
- control stays close to the process
- telemetry storage/querying stays in time-series-native tooling
- dashboards do not need to overload Node-RED itself
### 5.2 Edge-first matches operational reality
For wastewater/process systems, edge-first remains correct:
- lower latency
- better degraded-mode behavior
- less dependence on WAN or central platform uptime
- clearer OT trust boundary
### 5.3 Site mediation improves safety and security
Using central as the enterprise/API entry point and site as the mediator improves posture:
- field systems are less exposed
- policy decisions can be centralized
- external integrations do not probe the edge directly
- site can continue operating even when upstream is degraded
### 5.4 Multi-level storage enables better analytics
Multiple Influx layers can support:
- local resilience
- site diagnostics
- fleet benchmarking
- smarter retention and reconstruction strategies
That is substantially more capable than a single central historian model.
### 5.5 `tagcodering` is the right long-term direction
A database-backed configuration authority is stronger than embedding configuration only in flows because it supports:
- machine metadata management
- controlled rollout of configuration changes
- clearer versioning and provenance
- future API-driven configuration services
## 6. Downsides And Risks
### 6.1 Smart storage raises algorithmic and governance complexity
Signal-aware storage and reconstruction is promising, but it creates architectural obligations:
- reconstruction rules must be explicit
- acceptable reconstruction error must be defined per signal type
- operators must know whether they see raw or reconstructed history
- compliance-relevant data may need stricter retention than operational convenience data
Without those rules, smart storage can become opaque and hard to trust.
### 6.2 Multi-level databases can create ownership confusion
If edge, site, and central all store telemetry, you must define:
- which layer is authoritative for which time horizon
- when backfill is allowed
- when data is summarized vs copied
- how duplicates or gaps are detected
Otherwise operations will argue over which trend is "the real one."
### 6.3 Central intelligence must remain advisory-first
Central guidance can become valuable, but direct closed-loop dependency on central would be risky.
The architecture should therefore preserve:
- local control authority at edge/site
- bounded and explicit central advice
- safe behavior if central recommendations stop arriving
### 6.4 `tagcodering` is not yet complete enough to lean on blindly
It is the right target, but its current partial state means there is still architecture debt:
- incomplete config workflows
- likely mismatch between desired and implemented schema behavior
- temporary duplication between flows, node config, and database-held metadata
This should be treated as a core platform workstream, not a side issue.
### 6.5 Broker responsibilities are still not crisp enough
The materials still reference MQTT/AMQP/RabbitMQ/brokers without one stable responsibility split. That needs to be resolved before large-scale deployment.
Questions still open:
- command bus or event bus?
- site-only or cross-site?
- telemetry transport or only synchronization/eventing?
- durability expectations and replay behavior?
## 7. Security And Regulatory Positioning
### 7.1 Purdue-style layering is a good fit
EVOLV's preferred structure aligns well with a Purdue-style OT/IT layering approach:
- PLCs and field assets stay at the operational edge
- edge runtimes stay close to the process
- site systems mediate between OT and broader enterprise concerns
- central services host APIs, identity, analytics, and engineering workflows
That is important because it supports segmented trust boundaries instead of direct enterprise-to-field reach-through.
### 7.2 NIS2 alignment
Directive (EU) 2022/2555 (NIS2) requires cybersecurity risk-management measures, incident handling, and stronger governance for covered entities.
This architecture supports that by:
- limiting direct exposure of field systems
- separating operational layers
- enabling central policy and oversight
- preserving local operation during upstream failure
### 7.3 CER alignment
Directive (EU) 2022/2557 (Critical Entities Resilience Directive) focuses on resilience of essential services.
The edge-plus-site approach supports that direction because:
- local/site layers can continue during central disruption
- essential service continuity does not depend on one central runtime
- degraded-mode behavior can be explicitly designed per layer
### 7.4 Cyber Resilience Act alignment
Regulation (EU) 2024/2847 (Cyber Resilience Act) creates cybersecurity requirements for products with digital elements.
For EVOLV, that means the platform should keep strengthening:
- secure configuration handling
- vulnerability and update management
- release traceability
- lifecycle ownership of components and dependencies
### 7.5 GDPR alignment where personal data is present
Regulation (EU) 2016/679 (GDPR) applies whenever EVOLV processes personal data.
The architecture helps by:
- centralizing ingress
- reducing unnecessary propagation of data to field layers
- making access, retention, and audit boundaries easier to define
### 7.6 What can and cannot be claimed
The defensible claim is that EVOLV can be deployed in a way that supports compliance with strict European cybersecurity and resilience expectations.
The non-defensible claim is that EVOLV is automatically compliant purely because of the architecture diagram.
Actual compliance still depends on implementation and operations, including:
- access control
- patch and vulnerability management
- incident response
- logging and audit evidence
- retention policy
- data classification
## 8. Recommended Ideal Stack
The ideal EVOLV stack should be layered around operational boundaries, not around tools.
### 7.1 Layer A: Edge execution
Purpose:
- connect to PLCs and field assets
- execute time-sensitive local logic
- preserve operation during WAN/central loss
- provide local telemetry access for resilience and digital-twin use cases
Recommended components:
- Node-RED runtime for EVOLV edge flows
- OPC UA and protocol adapters
- local InfluxDB
- optional local Grafana for local engineering/monitoring
- optional local broker only when multiple participants need decoupling
Principle:
- edge remains safe and useful when disconnected
### 7.2 Layer B: Site integration
Purpose:
- aggregate multiple edge systems at plant/site level
- host plant-local dashboards and diagnostics
- mediate between raw OT detail and central standardization
- serve as the protected step between field systems and central requests
Recommended components:
- site Node-RED / CoreSync services
- site InfluxDB
- site Grafana / SCADA-supporting dashboards
- site broker where asynchronous eventing is justified
Principle:
- site absorbs plant complexity and protects field assets
### 7.3 Layer C: Central platform
Purpose:
- fleet-wide analytics
- shared dashboards
- engineering lifecycle
- enterprise/API entry point
- overview intelligence and advisory logic
Recommended components:
- Gitea
- CI/CD
- central InfluxDB
- central Grafana
- API/integration gateway
- IAM
- VPN/private connectivity
- `tagcodering`-backed configuration services
Principle:
- central coordinates, advises, and governs; it is not the direct field caller
### 7.4 Cross-cutting platform services
These should be explicit architecture elements:
- secrets management
- certificate management
- backup/restore
- audit logging
- monitoring/alerting of the platform itself
- versioned configuration and schema management
- rollout/rollback strategy
## 9. Recommended Opinionated Choices
### 8.1 Keep Node-RED as the orchestration layer, not the whole platform
Node-RED should own:
- process orchestration
- protocol mediation
- edge/site logic
- KPI production
It should not become the sole owner of:
- identity
- long-term configuration authority
- secret management
- compliance/audit authority
### 8.2 Use InfluxDB by function and horizon
Recommended split:
- edge: resilience, local replay, digital-twin input
- site: plant diagnostics and local continuity
- central: fleet analytics, advisory intelligence, benchmarking, and long-term cross-site views
### 8.3 Prefer smart telemetry retention over naive point dumping
Recommended rule:
- keep information-rich points
- reduce information-poor flat spans
- document reconstruction assumptions
- define signal-class-specific fidelity expectations
This needs design discipline, but it is a real differentiator if executed well.
### 8.4 Put enterprise/API ingress at central, not at edge
This should become a hard architectural rule:
- external requests land centrally
- central authenticates and authorizes
- central or site mediates downward
- edge never becomes the exposed public integration surface
### 8.5 Make `tagcodering` the target configuration backbone
The architecture should be designed so that `tagcodering` can mature into:
- machine and asset registry
- configuration source of truth
- site/central configuration exchange point
- API-served configuration source for runtime layers
## 10. Suggested Phasing
### Phase 1: Stabilize contracts
- define topic and payload contracts
- define telemetry classes and reconstruction policy
- define asset, machine, and site identity model
- define `tagcodering` scope and schema ownership
### Phase 2: Harden local/site resilience
- formalize edge and site runtime patterns
- define local telemetry retention and replay behavior
- define central-loss behavior
- define dashboard behavior during isolation
### Phase 3: Harden central platform
- IAM
- API gateway
- central observability
- CI/CD
- backup and disaster recovery
- config services over `tagcodering`
### Phase 4: Introduce selective synchronization and intelligence
- event-driven telemetry propagation rules
- smart-storage promotion/backfill policies
- advisory services from central
- auditability of downward recommendations and configuration changes
## 11. Immediate Open Questions Before Wiki Finalization
1. Which signals are allowed to use reconstruction-aware smart storage, and which must remain raw or near-raw for audit/compliance reasons?
2. How should `tagcodering` be exposed to runtime layers: direct database access, a dedicated API, or both?
3. What exact responsibility split should EVOLV use between API synchronization and broker-based eventing?
## 12. Recommended Wiki Structure
The wiki should not be one long page. It should be split into:
1. platform overview with the main topology diagram
2. edge-site-central runtime model
3. telemetry and smart storage model
4. security and access-boundary model
5. configuration architecture centered on `tagcodering`
## 13. Next Step
Use this document as the architecture baseline. The companion markdown page in `architecture/` can then be shaped into a wiki-ready visual overview page with Mermaid diagrams and shorter human-readable sections.

@@ -1,462 +0,0 @@
---
title: generalFunctions API Reference
created: 2026-03-01
updated: 2026-04-07
status: evolving
tags: [api, generalFunctions, reference]
---
> **⚠️ ARCHIVED — pre-refactor (Tier 14, 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.**
# generalFunctions API Reference
Shared library (`nodes/generalFunctions/`) used across all EVOLV Node-RED nodes.
```js
const { logger, outputUtils, MeasurementContainer, ... } = require('generalFunctions');
```
---
## Table of Contents
1. [Logger](#logger)
2. [OutputUtils](#outpututils)
3. [ValidationUtils](#validationutils)
4. [MeasurementContainer](#measurementcontainer)
5. [ConfigManager](#configmanager)
6. [ChildRegistrationUtils](#childregistrationutils)
7. [MenuUtils](#menuutils)
8. [EndpointUtils](#endpointutils)
9. [Positions](#positions)
10. [AssetLoader / loadCurve](#assetloader--loadcurve)
---
## Logger
Structured, level-filtered console logger.
**File:** `src/helper/logger.js`
### Constructor
```js
new Logger(logging = true, logLevel = 'debug', nameModule = 'N/A')
```
| Param | Type | Default | Description |
|---|---|---|---|
| `logging` | `boolean` | `true` | Enable/disable all output |
| `logLevel` | `string` | `'debug'` | Minimum severity: `'debug'` \| `'info'` \| `'warn'` \| `'error'` |
| `nameModule` | `string` | `'N/A'` | Label prefixed to every message |
### Methods
| Method | Signature | Description |
|---|---|---|
| `debug` | `(message: string): void` | Log at DEBUG level |
| `info` | `(message: string): void` | Log at INFO level |
| `warn` | `(message: string): void` | Log at WARN level |
| `error` | `(message: string): void` | Log at ERROR level |
| `setLogLevel` | `(level: string): void` | Change minimum level at runtime |
| `toggleLogging` | `(): void` | Flip logging on/off |
### Example
```js
const Logger = require('generalFunctions').logger;
const log = new Logger(true, 'info', 'MyNode');
log.info('Node started'); // [INFO] -> MyNode: Node started
log.debug('ignored'); // silent (below 'info')
log.setLogLevel('debug');
log.debug('now visible'); // [DEBUG] -> MyNode: now visible
```
---
## OutputUtils
Tracks output state and formats messages for InfluxDB or process outputs. Only emits changed fields.
**File:** `src/helper/outputUtils.js`
### Constructor
```js
new OutputUtils() // no parameters
```
### Methods
| Method | Signature | Returns | Description |
|---|---|---|---|
| `formatMsg` | `(output, config, format)` | `object \| undefined` | Diff against last output; returns formatted msg or `undefined` if nothing changed |
| `checkForChanges` | `(output, format)` | `object` | Returns only the key/value pairs that changed since last call |
**`format`** must be `'influxdb'` or `'process'`.
### Example
```js
const out = new OutputUtils();
const msg = out.formatMsg(
{ temperature: 22.5, pressure: 1013 },
config,
'influxdb'
);
// msg = { topic: 'nodeName', payload: { measurement, fields, tags, timestamp } }
```
---
## ValidationUtils
Schema-driven config validation with type coercion, range clamping, and nested object support.
**File:** `src/helper/validationUtils.js`
### Constructor
```js
new ValidationUtils(loggerEnabled = true, loggerLevel = 'warn')
```
### Methods
| Method | Signature | Returns | Description |
|---|---|---|---|
| `validateSchema` | `(config, schema, name)` | `object` | Walk the schema, validate every field, return a clean config. Unknown keys are stripped. Missing keys get their schema default. |
| `constrain` | `(value, min, max)` | `number` | Clamp a numeric value to `[min, max]` |
| `removeUnwantedKeys` | `(obj)` | `object` | Strip `rules`/`description` metadata, collapse `default` values |
**Supported `rules.type` values:** `number`, `integer`, `boolean`, `string`, `enum`, `array`, `set`, `object`, `curve`, `machineCurve`.
### Example
```js
const ValidationUtils = require('generalFunctions').validation;
const v = new ValidationUtils(true, 'warn');
const schema = {
temperature: { default: 20, rules: { type: 'number', min: -40, max: 100 } },
unit: { default: 'C', rules: { type: 'enum', values: [{ value: 'C' }, { value: 'F' }] } }
};
const validated = v.validateSchema({ temperature: 999 }, schema, 'myNode');
// validated.temperature === 100 (clamped)
// validated.unit === 'C' (default applied)
```
---
## MeasurementContainer
Chainable measurement storage organised by **type / variant / position**. Supports auto unit conversion, windowed statistics, events, and positional difference calculations.
**File:** `src/measurements/MeasurementContainer.js`
### Constructor
```js
new MeasurementContainer(options = {}, logger)
```
| Option | Type | Default | Description |
|---|---|---|---|
| `windowSize` | `number` | `10` | Rolling window for statistics |
| `defaultUnits` | `object` | `{ pressure:'mbar', flow:'m3/h', ... }` | Default unit per measurement type |
| `autoConvert` | `boolean` | `true` | Auto-convert values to target unit |
| `preferredUnits` | `object` | `{}` | Per-type unit overrides |
### Chainable Setters
All return `this` for chaining.
```js
container
.type('pressure')
.variant('static')
.position('upstream')
.distance(5)
.unit('bar')
.value(3.2, Date.now(), 'bar');
```
| Method | Signature | Description |
|---|---|---|
| `type` | `(typeName): this` | Set measurement type (e.g. `'pressure'`) |
| `variant` | `(variantName): this` | Set variant (e.g. `'static'`, `'differential'`) |
| `position` | `(positionValue): this` | Set position (e.g. `'upstream'`, `'downstream'`) |
| `distance` | `(distance): this` | Set physical distance from parent |
| `unit` | `(unitName): this` | Set unit on the underlying measurement |
| `value` | `(val, timestamp?, sourceUnit?): this` | Store a value; auto-converts if `sourceUnit` differs from target |
### Terminal / Query Methods
| Method | Signature | Returns | Description |
|---|---|---|---|
| `get` | `()` | `Measurement \| null` | Get the raw measurement object |
| `getCurrentValue` | `(requestedUnit?)` | `number \| null` | Latest value, optionally converted |
| `getAverage` | `(requestedUnit?)` | `number \| null` | Windowed average |
| `getMin` | `()` | `number \| null` | Window minimum |
| `getMax` | `()` | `number \| null` | Window maximum |
| `getAllValues` | `()` | `array \| null` | All stored samples |
| `getLaggedValue` | `(lag?, requestedUnit?)` | `number \| null` | Value from `lag` samples ago |
| `getLaggedSample` | `(lag?, requestedUnit?)` | `object \| null` | Full sample `{ value, timestamp, unit }` from `lag` samples ago |
| `exists` | `({ type?, variant?, position?, requireValues? })` | `boolean` | Check if a measurement series exists |
| `difference` | `({ from?, to?, unit? })` | `object \| null` | Compute `{ value, avgDiff, unit }` between two positions |
### Introspection / Lifecycle
| Method | Signature | Returns | Description |
|---|---|---|---|
| `getTypes` | `()` | `string[]` | All registered measurement types |
| `getVariants` | `()` | `string[]` | Variants under current type |
| `getPositions` | `()` | `string[]` | Positions under current type+variant |
| `getAvailableUnits` | `(measurementType?)` | `string[]` | Units available for a type |
| `getBestUnit` | `(excludeUnits?)` | `object \| null` | Best human-readable unit for current value |
| `setPreferredUnit` | `(type, unit)` | `this` | Override default unit for a type |
| `setChildId` | `(id)` | `this` | Tag container with a child node ID |
| `setChildName` | `(name)` | `this` | Tag container with a child node name |
| `setParentRef` | `(parent)` | `this` | Store reference to parent node |
| `clear` | `()` | `void` | Reset all measurements and chain state |
### Events
The internal `emitter` fires `"type.variant.position"` on every `value()` call with:
```js
{ value, originalValue, unit, sourceUnit, timestamp, position, distance, variant, type, childId, childName, parentRef }
```
### Example
```js
const { MeasurementContainer } = require('generalFunctions');
const mc = new MeasurementContainer({ windowSize: 5 });
mc.type('pressure').variant('static').position('upstream').value(3.2);
mc.type('pressure').variant('static').position('downstream').value(2.8);
const diff = mc.type('pressure').variant('static').difference();
// diff = { value: -0.4, avgDiff: -0.4, unit: 'mbar', from: 'downstream', to: 'upstream' }
```
---
## ConfigManager
Loads JSON config files from disk and builds merged runtime configs.
**File:** `src/configs/index.js`
### Constructor
```js
new ConfigManager(relPath = '.')
```
`relPath` is resolved relative to the configs directory.
### Methods
| Method | Signature | Returns | Description |
|---|---|---|---|
| `getConfig` | `(configName)` | `object` | Load and parse `<configName>.json` |
| `getAvailableConfigs` | `()` | `string[]` | List config names (without `.json`) |
| `hasConfig` | `(configName)` | `boolean` | Check existence |
| `getBaseConfig` | `()` | `object` | Shortcut for `getConfig('baseConfig')` |
| `buildConfig` | `(nodeName, uiConfig, nodeId, domainConfig?)` | `object` | Merge base schema + UI overrides into a runtime config |
| `createEndpoint` | `(nodeName)` | `string` | Generate browser JS that injects config into `window.EVOLV.nodes` |
### Example
```js
const { configManager } = require('generalFunctions');
const cfg = configManager.buildConfig('measurement', uiConfig, node.id, {
scaling: { enabled: true, inputMin: 0, inputMax: 100 }
});
```
---
## ChildRegistrationUtils
Manages parent-child node relationships: registration, lookup, and structure storage.
**File:** `src/helper/childRegistrationUtils.js`
### Constructor
```js
new ChildRegistrationUtils(mainClass)
```
`mainClass` is the parent node instance (must expose `.logger` and optionally `.registerChild()`).
### Methods
| Method | Signature | Returns | Description |
|---|---|---|---|
| `registerChild` | `(child, positionVsParent, distance?)` | `Promise<any>` | Register a child node under the parent. Sets up parent refs, measurement context, and stores by softwareType/category. |
| `getChildrenOfType` | `(softwareType, category?)` | `array` | Get children filtered by software type and optional category |
| `getChildById` | `(childId)` | `object \| null` | Lookup a single child by its ID |
| `getAllChildren` | `()` | `array` | All registered children |
| `logChildStructure` | `()` | `void` | Debug-print the full child tree |
### Example
```js
const { childRegistrationUtils: CRU } = require('generalFunctions');
const cru = new CRU(parentNode);
await cru.registerChild(sensorNode, 'upstream');
cru.getChildrenOfType('measurement'); // [sensorNode]
```
---
## MenuUtils
Browser-side UI helper for Node-RED editor. Methods are mixed in from separate modules: toggles, data fetching, URL utils, dropdown population, and HTML generation.
**File:** `src/helper/menuUtils.js`
### Constructor
```js
new MenuUtils() // no parameters; sets isCloud=false, configData=null
```
### Key Methods
**Toggles** -- control UI element visibility:
| Method | Signature | Description |
|---|---|---|
| `initBasicToggles` | `(elements)` | Bind log-level row visibility to log checkbox |
| `initMeasurementToggles` | `(elements)` | Bind scaling input rows to scaling checkbox |
| `initTensionToggles` | `(elements, node)` | Show/hide tension row based on interpolation method |
**Data Fetching:**
| Method | Signature | Returns | Description |
|---|---|---|---|
| `fetchData` | `(url, fallbackUrl)` | `Promise<array>` | Fetch JSON from primary URL; fall back on failure |
| `fetchProjectData` | `(url)` | `Promise<object>` | Fetch project-level data |
| `apiCall` | `(node)` | `Promise<object>` | POST to asset-register API |
**URL Construction:**
| Method | Signature | Returns | Description |
|---|---|---|---|
| `getSpecificConfigUrl` | `(nodeName, cloudAPI)` | `{ cloudConfigURL, localConfigURL }` | Build cloud + local config URLs |
| `constructUrl` | `(base, ...paths)` | `string` | Join URL segments safely |
| `constructCloudURL` | `(base, ...paths)` | `string` | Same as `constructUrl`, for cloud endpoints |
**Dropdown Population:**
| Method | Signature | Description |
|---|---|---|
| `fetchAndPopulateDropdowns` | `(configUrls, elements, node)` | Cascading supplier > subType > model > unit dropdowns |
| `populateDropdown` | `(htmlElement, options, node, property, callback?)` | Fill a `<select>` with options and wire change events |
| `populateLogLevelOptions` | `(logLevelSelect, configData, node)` | Populate log-level dropdown from config |
| `populateSmoothingMethods` | `(configUrls, elements, node)` | Populate smoothing method dropdown |
| `populateInterpolationMethods` | `(configUrls, elements, node)` | Populate interpolation method dropdown |
| `generateHtml` | `(htmlElement, options, savedValue)` | Write `<option>` HTML into an element |
---
## EndpointUtils
Server-side helper that serves `MenuUtils` as browser JavaScript via Node-RED HTTP endpoints.
**File:** `src/helper/endpointUtils.js`
### Constructor
```js
new EndpointUtils({ MenuUtilsClass? })
```
| Param | Type | Default | Description |
|---|---|---|---|
| `MenuUtilsClass` | `class` | `MenuUtils` | The MenuUtils constructor to introspect |
### Methods
| Method | Signature | Returns | Description |
|---|---|---|---|
| `createMenuUtilsEndpoint` | `(RED, nodeName, customHelpers?)` | `void` | Register `GET /<nodeName>/resources/menuUtils.js` |
| `generateMenuUtilsCode` | `(nodeName, customHelpers?)` | `string` | Produce the browser JS string (introspects `MenuUtils.prototype`) |
### Example
```js
const EndpointUtils = require('generalFunctions/src/helper/endpointUtils');
const ep = new EndpointUtils();
ep.createMenuUtilsEndpoint(RED, 'valve');
// Browser can now load: GET /valve/resources/menuUtils.js
```
---
## Positions
Canonical constants for parent-child spatial relationships.
**File:** `src/constants/positions.js`
### Exports
```js
const { POSITIONS, POSITION_VALUES, isValidPosition } = require('generalFunctions');
```
| Export | Type | Value |
|---|---|---|
| `POSITIONS` | `object` | `{ UPSTREAM: 'upstream', DOWNSTREAM: 'downstream', AT_EQUIPMENT: 'atEquipment', DELTA: 'delta' }` |
| `POSITION_VALUES` | `string[]` | `['upstream', 'downstream', 'atEquipment', 'delta']` |
| `isValidPosition` | `(pos: string): boolean` | Returns `true` if `pos` is one of the four values |
---
## AssetLoader / loadCurve
Loads JSON asset files (machine curves, etc.) from the datasets directory with LRU caching.
**File:** `datasets/assetData/curves/index.js`
### Singleton convenience functions
```js
const { loadCurve } = require('generalFunctions');
```
| Function | Signature | Returns | Description |
|---|---|---|---|
| `loadCurve` | `(curveType: string)` | `object \| null` | Load `<curveType>.json` from the curves directory |
| `loadAsset` | `(datasetType, assetId)` | `object \| null` | Load any JSON asset by dataset folder and ID |
| `getAvailableAssets` | `(datasetType)` | `string[]` | List asset IDs in a dataset folder |
### AssetLoader class
```js
new AssetLoader(maxCacheSize = 100)
```
Same methods as above (`loadCurve`, `loadAsset`, `getAvailableAssets`), plus `clearCache()`.
### Example
```js
const { loadCurve } = require('generalFunctions');
const curve = loadCurve('hidrostal-H05K-S03R');
// curve = { flow: [...], head: [...], ... } or null
```

@@ -1,28 +0,0 @@
> **⚠️ ARCHIVED — pre-refactor (Tier 14, 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.**
# Source Documents
Place actual scientific papers, standards, and technical manuals here. Reference them from the summary files in the parent directory.
## Suggested Sources to Add
- IWA Scientific and Technical Report No. 1 — ASM1 (Henze et al., 1987)
- IWA Scientific and Technical Report No. 3 — ASM2d (Henze et al., 1999)
- IWA Scientific and Technical Report No. 9 — ASM3 (Gujer et al., 1999)
- Takacs et al. (1991) "A dynamic model of the clarification-thickening process" Water Res. 25(10), 1263-1271
- Astrom & Hagglund (2006) "Advanced PID Control" ISA
- Karassik et al. "Pump Handbook" McGraw-Hill
- Europump/Hydraulic Institute "Pump Life Cycle Costs"
- IEC 62443 series (OT security)
- IEC 61298 series (process measurement)
- EU Directive 91/271/EEC (Urban Waste Water Treatment)
- NIST SP 800-82 Rev 3 (Guide to ICS Security)
## File Naming Convention
`<author-year>-<short-title>.pdf` — e.g., `takacs-1991-clarification-thickening.pdf`

@@ -1,96 +0,0 @@
---
title: Open Issues — EVOLV Codebase
created: 2026-03-01
updated: 2026-04-07
status: evolving
tags: [issues, backlog]
---
> **⚠️ ARCHIVED — pre-refactor (Tier 14, 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.**
# Open Issues — EVOLV Codebase
Issues identified during codebase scan (2026-03-12). Create these on Gitea when ready.
---
## Issue 1: Restore diffuser node implementation
**Labels:** `enhancement`, `node`
**Priority:** Medium
The `nodes/diffuser/` directory contains only `.git`, `LICENSE`, and `README.md` — no implementation. There was a previous experimental version. Needs:
- Retrieve original diffuser logic from user/backup
- Rebuild to current three-layer architecture (wrapper `.js` + `src/nodeClass.js` + `src/specificClass.js`)
- Use `require('generalFunctions')` barrel imports
- Add config JSON in `generalFunctions/src/configs/diffuser.json`
- Register under category `'EVOLV'` with appropriate S88 color
- Add tests
**Blocked on:** User providing original diffuser logic/requirements.
---
## Issue 2: Relocate prediction/ML modules to external service
**Labels:** `enhancement`, `architecture`
**Priority:** Medium
TensorFlow-based influent prediction code was removed from monster node (was broken/incomplete). The prediction functionality needs a new home:
- LSTM model for 24-hour flow prediction based on precipitation data
- Standardization constants: hours `(mean=11.504, std=6.922)`, precipitation `(mean=0.090, std=0.439)`, response `(mean=1188.01, std=1024.19)`
- Model was served from `http://127.0.0.1:1880/generalFunctions/datasets/lstmData/tfjs_model/`
- Consider: separate microservice, Python-based inference, or ONNX runtime
- Monster node should accept predictions via `model_prediction` message topic from external service
**Related files removed:** `monster_class.js` methods `get_model_prediction()`, `model_loader()`
---
## Issue 3: Modernize monster node to three-layer architecture
**Labels:** `refactor`, `node`
**Priority:** Low
Monster node uses old-style structure (`dependencies/monster/` instead of `src/`). Should be refactored:
- Move `dependencies/monster/monster_class.js``src/specificClass.js`
- Create `src/nodeClass.js` adapter (extract from `monster.js`)
- Slim down `monster.js` to standard wrapper pattern
- Move `monsterConfig.json``generalFunctions/src/configs/monster.json`
- Remove `modelLoader.js` (TF dependency removed)
- Add unit tests
**Note:** monster_class.js is ~500 lines of domain logic. Keep sampling_program(), aggregation, AQUON integration intact.
---
## Issue 4: Clean up inline test/demo code in specificClass files
**Labels:** `cleanup`
**Priority:** Low
Several specificClass files have test/demo code after `module.exports`:
- `pumpingStation/src/specificClass.js` (lines 478-697): Demo code guarded with `require.main === module` — acceptable but could move to `test/` or `examples/`
- `machineGroupControl/src/specificClass.js` (lines 969-1158): Block-commented test code with `makeMachines()` — dead code, could be removed or moved to test file
---
## Issue 5: DashboardAPI node improvements
**Labels:** `enhancement`, `security`
**Priority:** Low
- Bearer token now relies on `GRAFANA_TOKEN` env var (hardcoded token was removed for security)
- Ensure deployment docs mention setting `GRAFANA_TOKEN`
- `dashboardapi_class.js` still has `console.log` calls (lines 154, 178) — should use logger
- Node doesn't follow three-layer architecture (older style)

@@ -1,65 +0,0 @@
---
title: Wiki Index
updated: 2026-04-13
---
> **⚠️ ARCHIVED — pre-refactor (Tier 14, 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 Project Wiki Index
## Overview
- [Project Overview](overview.md) — what works, what doesn't, node inventory
- [Metrics Dashboard](metrics.md) — test counts, power comparison, performance
- [Knowledge Graph](knowledge-graph.yaml) — structured data, machine-queryable
## Architecture
- [Node Architecture](architecture/node-architecture.md) — three-layer pattern, ports, mermaid diagrams
- [3D Pump Curves](architecture/3d-pump-curves.md) — predict class, spline interpolation, unit chain
- [Group Optimization](architecture/group-optimization.md) — BEP-Gravitation, combination selection, marginal-cost refinement
- [Platform Overview](architecture/platform-overview.md) — edge/site/central layering, telemetry model
- [Deployment Blueprint](architecture/deployment-blueprint.md) — Docker topology, rollout order
- [Stack Review](architecture/stack-review.md) — full stack architecture assessment
## Core Concepts
- [generalFunctions API](concepts/generalfunctions-api.md) — logger, MeasurementContainer, configManager, etc.
- [Pump Affinity Laws](concepts/pump-affinity-laws.md) — Q ∝ N, H ∝ N², P ∝ N³
- [ASM Models](concepts/asm-models.md) — activated sludge model kinetics
- [PID Control Theory](concepts/pid-control-theory.md) — proportional-integral-derivative control
- [Settling Models](concepts/settling-models.md) — secondary clarifier sludge settling
- [Signal Processing for Sensors](concepts/signal-processing-sensors.md) — sensor conditioning
- [InfluxDB Schema Design](concepts/influxdb-schema-design.md) — telemetry data model
- [OT Security (IEC 62443)](concepts/ot-security-iec62443.md) — industrial security standard
- [Wastewater Compliance NL](concepts/wastewater-compliance-nl.md) — Dutch regulatory requirements
## Findings
- [BEP-Gravitation Proof](findings/bep-gravitation-proof.md) — within 0.1% of brute-force optimum (proven)
- [NCog Behavior](findings/ncog-behavior.md) — when NCog works, when it's zero, how it's used (evolving)
- [Curve Non-Convexity](findings/curve-non-convexity.md) — C5 sparse data artifacts (proven)
- [Pump Switching Stability](findings/pump-switching-stability.md) — 1-2 transitions, no hysteresis (proven)
- [Open Issues (2026-03)](findings/open-issues-2026-03.md) — diffuser, monster refactor, ML relocation, etc.
## Manuals
- [rotatingMachine User Manual](manuals/nodes/rotatingMachine.md) — inputs, outputs, state machine, examples
- [measurement User Manual](manuals/nodes/measurement.md) — analog + digital modes, smoothing, outlier filtering
- [FlowFuse Dashboard Layout](manuals/node-red/flowfuse-dashboard-layout-manual.md)
- [FlowFuse Widget Catalog](manuals/node-red/flowfuse-widgets-catalog.md)
- [Node-RED Function Patterns](manuals/node-red/function-node-patterns.md)
- [Node-RED Runtime](manuals/node-red/runtime-node-js.md)
- [Messages and Editor Structure](manuals/node-red/messages-and-editor-structure.md)
## Sessions
- [2026-04-07: Production Hardening](sessions/2026-04-07-production-hardening.md) — rotatingMachine + machineGroupControl
- [2026-04-13: rotatingMachine Trial-Ready](sessions/2026-04-13-rotatingMachine-trial-ready.md) — FSM interruptibility, config schema sync, UX polish, dual-curve tests
- [2026-04-13: measurement Digital Mode](sessions/2026-04-13-measurement-digital-mode.md) — silent dispatcher bug fix, 59 new tests, MQTT-style multi-channel input mode
## Other Documentation (outside wiki)
- `CLAUDE.md` — Claude Code project guide (root)
- `.agents/AGENTS.md` — agent routing table, orchestrator policy
- `.agents/` — skills, decisions, function-anchors, improvements
- `.claude/` — Claude Code agents and rules

@@ -1,168 +0,0 @@
> **⚠️ ARCHIVED — pre-refactor (Tier 14, 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.**
# Knowledge Graph — structured data with provenance
# Every claim has: value, source (file/commit), date, status
# ── TESTS ──
tests:
rotatingMachine:
basic:
count: 10
passing: 10
file: nodes/rotatingMachine/test/basic/
date: 2026-04-07
integration:
count: 16
passing: 16
file: nodes/rotatingMachine/test/integration/
date: 2026-04-07
edge:
count: 17
passing: 17
file: nodes/rotatingMachine/test/edge/
date: 2026-04-07
machineGroupControl:
basic:
count: 1
passing: 1
file: nodes/machineGroupControl/test/basic/
date: 2026-04-07
integration:
count: 3
passing: 3
file: nodes/machineGroupControl/test/integration/
date: 2026-04-07
edge:
count: 1
passing: 1
file: nodes/machineGroupControl/test/edge/
date: 2026-04-07
# ── METRICS ──
metrics:
optimization_gap_to_brute_force:
value: "0.1% max"
source: distribution-power-table.integration.test.js
date: 2026-04-07
conditions: "3 pumps, 1000-step brute force, 0.05% flow tolerance"
optimization_time_median:
value: "0.027-0.153ms"
source: benchmark script
date: 2026-04-07
conditions: "3 pumps, 6 combinations, BEP-Gravitation + refinement"
pump_switching_stability:
value: "1-2 transitions across 5-95% demand"
source: stability sweep
date: 2026-04-07
conditions: "2% demand steps, both ascending and descending"
pump_curves:
H05K-S03R:
pressure_levels: 33
pressure_range: "700-3900 mbar"
flow_range: "28-227 m3/h (at 2000 mbar)"
data_points_per_level: 5
anomalies_fixed: 3
date: 2026-04-07
C5-D03R-SHN1:
pressure_levels: 26
pressure_range: "400-2900 mbar"
flow_range: "6-53 m3/h"
data_points_per_level: 5
non_convex: true
date: 2026-04-07
# ── DISPROVEN CLAIMS ──
disproven:
ncog_proportional_weight:
claimed: "Distributing flow proportional to NCog weights is optimal"
claimed_date: 2026-04-07
disproven_date: 2026-04-07
evidence_for: "Simple implementation in calcBestCombination"
evidence_against: "Starves small pumps (NCog=0 gets zero flow), overloads large pumps at high demand. BEP-target + scale is correct approach."
root_cause: "NCog is a position indicator (0-1 on flow range), not a distribution weight"
efficiency_rounding:
claimed: "Math.round(flow/power * 100) / 100 preserves BEP signal"
claimed_date: pre-2026-04-07
disproven_date: 2026-04-07
evidence_for: "Removes floating point noise"
evidence_against: "In canonical units (m3/s and W), Q/P ratio is ~1e-6. Rounding to 2 decimals produces 0 for all points. NCog, cog, BEP all became 0."
root_cause: "Canonical units make the ratio very small — rounding destroys the signal"
equal_marginal_cost_optimal:
claimed: "Equal dP/dQ across pumps guarantees global power minimum"
claimed_date: 2026-04-07
disproven_date: 2026-04-07
evidence_for: "KKT conditions for convex functions"
evidence_against: "C5 pump curve is non-convex (dP/dQ dips from 1.3M to 453K then rises). Sparse data (5 points) causes spline artifacts."
root_cause: "Convexity assumption fails with interpolated curves from sparse data"
# ── PERFORMANCE ──
performance:
mgc_optimization:
median_ms: 0.09
p99_ms: 0.5
tick_budget_pct: 0.015
source: benchmark script
date: 2026-04-07
predict_y_call:
complexity: "O(log n), ~O(1) for 5-10 data points"
source: predict_class.js
# ── ARCHITECTURE ──
architecture:
canonical_units:
pressure: Pa
flow: "m3/s"
power: W
temperature: K
output_units:
pressure: mbar
flow: "m3/h"
power: kW
temperature: C
node_count: 13
submodules: 12
# ── BUGS FIXED ──
bugs_fixed:
flowmovement_unit_mismatch:
severity: critical
description: "machineGroupControl sent flow in canonical (m3/s) but rotatingMachine flowmovement expected output units (m3/h). Every pump stayed at minimum."
fix: "_canonicalToOutputFlow() conversion before all flowmovement calls"
commit: d55f401
date: 2026-04-07
emergencystop_case:
severity: critical
description: "specificClass called executeSequence('emergencyStop') but config key was 'emergencystop'"
fix: "Lowercase to match config"
commit: 07af7ce
date: 2026-04-07
curve_data_anomalies:
severity: high
description: "3 flow values leaked into power column in hidrostal-H05K-S03R.json at pressures 1600, 3200, 3300 mbar"
fix: "Linearly interpolated correct values from adjacent levels"
commit: 024db55
date: 2026-04-07
efficiency_rounding:
severity: high
description: "Math.round(Q/P * 100) / 100 destroyed all NCog/BEP calculations"
fix: "Removed rounding, use raw ratio"
commit: 07af7ce
date: 2026-04-07
absolute_scaling_bug:
severity: high
description: "handleInput compared demandQout (always 0) instead of demandQ for max cap"
fix: "Reordered conditions, use demandQ throughout"
commit: d55f401
date: 2026-04-07
# ── TIMELINE ──
timeline:
- {date: 2026-04-07, commit: 024db55, desc: "Fix 3 anomalous power values in hidrostal curve"}
- {date: 2026-04-07, commit: 07af7ce, desc: "rotatingMachine production hardening: safety + prediction + 43 tests"}
- {date: 2026-04-07, commit: d55f401, desc: "machineGroupControl: unit fix + refinement + stability tests"}
- {date: 2026-04-07, commit: fd9d167, desc: "Update EVOLV submodule refs"}

@@ -1,19 +0,0 @@
---
title: Wiki Log
---
> **⚠️ ARCHIVED — pre-refactor (Tier 14, 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.**
# Wiki Log
## [2026-04-07] Wiki initialized | Full codebase scan + session findings
- Created overview, metrics, knowledge graph from production hardening session
- Architecture pages: 3D pump curves, group optimization
- Findings: BEP-Gravitation proof, NCog behavior, curve non-convexity, switching stability
- Session log: 2026-04-07 production hardening

@@ -1,211 +0,0 @@
---
title: measurement — User Manual
node: measurement
updated: 2026-04-13
status: trial-ready
---
> **⚠️ ARCHIVED — pre-refactor (Tier 14, 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.**
# measurement — User Manual
The `measurement` node is the sensor-side of every EVOLV flow. It takes raw signal data, applies offset / scaling / smoothing / outlier rejection, and publishes a conditioned value into the shared `MeasurementContainer`. A parent equipment node (rotatingMachine, pumpingStation, reactor, ...) subscribes automatically via the child-registration handshake on port 2.
## At a glance
| Item | Value |
|---|---|
| Node category | EVOLV |
| Inputs | 1 (message-driven) |
| Outputs | 3 — `process` / `dbase` / `parent` |
| Tick period | 1 s |
| Input modes | `analog` (default) — one scalar per msg. `digital` — object payload with many keys. |
| Smoothing methods | 12 (`none`, `mean`, `min`, `max`, `sd`, `lowPass`, `highPass`, `weightedMovingAverage`, `bandPass`, `median`, `kalman`, `savitzkyGolay`) |
| Outlier methods | 3 (`zScore`, `iqr`, `modifiedZScore`) |
## Choosing a mode
### Analog — one scalar per message (PLC / 4-20 mA)
The classic pattern — what the node did before v1.1. `msg.payload` is a single number. The node runs one offset → scaling → smoothing → outlier pipeline and emits exactly one MeasurementContainer slot keyed by the asset's type + position.
```json
{ "topic": "measurement", "payload": 12.34 }
```
Use when one Node-RED `measurement` node represents one physical sensor.
### Digital — object payload, many channels (MQTT / IoT / JSON)
Use when one Node-RED `measurement` node represents one physical **device** that publishes multiple readings. Common shapes:
```json
{ "topic": "measurement",
"payload": { "temperature": 22.5, "humidity": 45, "pressure": 1013 } }
```
```json
{ "topic": "measurement",
"payload": { "co2": 618, "voc": 122, "pm25": 8 } }
```
Each top-level key maps to a **channel** with its own `type`, `position`, `unit`, and pipeline parameters. Unknown keys are ignored (logged at debug).
## Configuration
### Common (both modes)
- **Asset** (menu): supplier, category, asset type (`assetType`), model, unit.
- **Logger** (menu): log level + enable flag.
- **Position** (menu): `upstream` / `atEquipment` / `downstream`, optional distance offset.
### Analog fields
| Field | Meaning |
|---|---|
| **Scaling** | enables linear interpolation from source range to process range |
| **Source Min / Max** | raw input bounds (e.g. `4` / `20` for mA) |
| **Input Offset** | additive bias applied before scaling |
| **Process Min / Max** | mapped output bounds (e.g. `0` / `3000` for mbar) |
| **Simulator** | internal random-walk source for testing |
| **Smoothing** | method (dropdown) |
| **Window** | smoothing window size |
### Digital fields
- **Input Mode**: set to `digital` in the dropdown.
- **Channels (JSON)**: array of channel definitions.
Each channel:
```json
{
"key": "temperature",
"type": "temperature",
"position": "atEquipment",
"unit": "C",
"scaling": { "enabled": false, "inputMin": 0, "inputMax": 1, "absMin": -50, "absMax": 150, "offset": 0 },
"smoothing": { "smoothWindow": 5, "smoothMethod": "mean" },
"outlierDetection": { "enabled": true, "method": "zScore", "threshold": 3 }
}
```
`scaling` / `smoothing` / `outlierDetection` are optional — missing sections inherit the top-level analog-mode fields. `key` is the JSON field name inside `msg.payload`; `type` is the MeasurementContainer axis — any string works, not just the physical-unit-backed defaults.
## Input topics
| Topic | Payload | Effect |
|---|---|---|
| `measurement` | number (analog) / object (digital) | drives the pipeline |
| `simulator` | — | toggle the internal random-walk simulator |
| `outlierDetection` | — | toggle outlier rejection |
| `calibrate` | — | set the offset so the current output matches `Source Min` (scaling on) or `Process Min` (scaling off). Requires a stable window — aborts if the signal is fluctuating. |
## Output ports
### Port 0 — process
Delta-compressed payload.
**Analog** shape:
```json
{ "mAbs": 4.2, "mPercent": 42, "totalMinValue": 0, "totalMaxValue": 100,
"totalMinSmooth": 0, "totalMaxSmooth": 4.2 }
```
**Digital** shape:
```json
{ "channels": {
"temperature": { "key": "temperature", "type": "temperature", "position": "atEquipment",
"unit": "C", "mAbs": 24, "mPercent": 37,
"totalMinValue": 22.5, "totalMaxValue": 25.5,
"totalMinSmooth": 22.5, "totalMaxSmooth": 24 },
"humidity": { ... },
"pressure": { ... }
} }
```
### Port 1 — dbase
InfluxDB line-protocol telemetry. Tags = asset metadata; fields = measurements. See [InfluxDB Schema Design](../../concepts/influxdb-schema-design.md).
### Port 2 — parent
`{ topic: "registerChild", payload: <nodeId>, positionVsParent, distance }` — emitted once ~200 ms after deploy so the parent equipment node registers this sensor.
## Pipeline per value
1. **Outlier check** (if enabled) — rejects via zScore / IQR / modifiedZScore. Rejected values never advance, don't update min/max, don't emit.
2. **Offset**`value + scaling.offset`.
3. **Scaling** (if enabled) — linear interpolation from `[inputMin, inputMax]` to `[absMin, absMax]` with boundary clamping.
4. **Smoothing** — current value pushed into the rolling window; the configured method produces the smoothed output.
5. **Min/Max tracking** — both raw (pre-smoothing) and smoothed min/max tracked for display.
6. **Constrain** — smoothed value clamped to `[absMin, absMax]`.
7. **Emit**`MeasurementContainer.type(...).variant('measured').position(...).distance(...).value(out, ts, unit)` triggers the event `<type>.measured.<position>` (lowercase) that the parent equipment subscribes to.
In digital mode, each channel runs this pipeline independently.
## Smoothing methods — quick reference
| Method | Use case |
|---|---|
| `none` | pass raw value through — useful for testing |
| `mean` | simple arithmetic average over window |
| `min` / `max` | worst-case / peak reporting |
| `sd` | outputs standard deviation (noise indicator) |
| `median` | outlier-resistant central tendency |
| `weightedMovingAverage` | later samples weighted higher |
| `lowPass` | EMA-style attenuation of high-frequency noise |
| `highPass` | emphasises rapid changes (step detection) |
| `bandPass` | `lowPass + highPass - raw` — band-of-interest filtering |
| `kalman` | recursive noise filter, converges to steady value |
| `savitzkyGolay` | polynomial smoothing over 5-point window |
## Outlier methods — quick reference
| Method | Best when |
|---|---|
| `zScore` | signal is approximately normal; threshold = # of SDs |
| `iqr` | signal is non-normal; robust to skewed distributions |
| `modifiedZScore` | small samples; uses median / MAD instead of mean / SD |
> **Historical bug fixed 2026-04-13:** The dispatcher compared against camelCase keys (`lowPass`, `zScore`, ...) but the validator lowercases enum values. Result: 4 smoothing methods and 2 outlier methods were silently no-ops when chosen from the editor — they fell through to the "unknown" branch and emitted the raw last value. Review any flow deployed before 2026-04-13 that relied on these methods.
## Unit policy
Unknown measurement types (anything not in the container's built-in `measureMap`: `pressure`, `flow`, `power`, `temperature`, `volume`, `length`, `mass`, `energy`) are accepted without unit compatibility checks. This lets digital channels use `humidity` (`%`), `co2` (`ppm`), arbitrary IoT units. Known types still validate strictly.
## Example flow (digital)
```json
[
{ "id": "dig", "type": "measurement",
"mode": "digital",
"channels": "[{\"key\":\"temperature\",\"type\":\"temperature\",\"position\":\"atEquipment\",\"unit\":\"C\",\"scaling\":{\"enabled\":false,\"absMin\":-50,\"absMax\":150},\"smoothing\":{\"smoothWindow\":5,\"smoothMethod\":\"mean\"}},{\"key\":\"humidity\",\"type\":\"humidity\",\"position\":\"atEquipment\",\"unit\":\"%\",\"scaling\":{\"enabled\":false,\"absMin\":0,\"absMax\":100},\"smoothing\":{\"smoothWindow\":5,\"smoothMethod\":\"mean\"}}]",
...
}
]
```
## Testing
```bash
cd nodes/measurement
npm test
```
71 tests — coverage includes every smoothing method, every outlier strategy, scaling, interpolation, constrain, calibration, stability, simulation, per-channel pipelines, digital-mode dispatch, malformed-channel handling, event emits.
End-to-end benchmark scripts live in the superproject at `/tmp/m_e2e_baseline.py` (analog) and `/tmp/m_digital_e2e.py` (digital). Run against a Dockerized Node-RED stack (`docker compose up -d nodered`).
## Production status
Trial-ready as of 2026-04-13 after the session that fixed the silent dispatcher bug and added digital mode. See [session 2026-04-13](../../sessions/2026-04-13-measurement-digital-mode.md) and the memory file `node_measurement.md`.

@@ -1,255 +0,0 @@
---
title: rotatingMachine — User Manual
node: rotatingMachine
updated: 2026-04-13
status: trial-ready
---
> **⚠️ ARCHIVED — pre-refactor (Tier 14, 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.**
# rotatingMachine — User Manual
The `rotatingMachine` node models a single pump, compressor, or blower. It runs an S88-style state machine, predicts flow and power from a supplier curve, and publishes process and telemetry data every second. It is the atomic control module beneath `machineGroupControl` and `pumpingStation`.
This manual is the operator-facing reference. For architecture and the 3-tier code layout see [Node Architecture](../../architecture/node-architecture.md); for curve theory see [3D Pump Curves](../../architecture/3d-pump-curves.md).
## At a glance
| Item | Value |
|---|---|
| Node category | EVOLV |
| Inputs | 1 (message-driven) |
| Outputs | 3 — `process` / `dbase` / `parent` |
| Tick period | 1 s |
| State machine | 10 states (S88) |
| Predictions | curve-backed (nq flow, np power, reversed nq for ctrl) |
| Canonical units | Pa, m³/s, W, K |
## Editor configuration
| Field | Default | Meaning |
|---|---|---|
| **Reaction Speed** | `1` | Ramp rate in controller-position units per second. `1` = 1 %/s. |
| **Startup Time** | `0` | Seconds in the `starting` state. |
| **Warmup Time** | `0` | Seconds in the protected `warmingup` state. |
| **Shutdown Time** | `0` | Seconds in the `stopping` state. |
| **Cooldown Time** | `0` | Seconds in the protected `coolingdown` state. |
| **Movement Mode** | `staticspeed` | `staticspeed` = linear ramp; `dynspeed` = ease-in/out. |
| **Process Output** | `process` | Port 0 payload format: `process` (delta-compressed) / `json` / `csv`. |
| **Database Output** | `influxdb` | Port 1 payload format: `influxdb` line protocol / `json` / `csv`. |
| **Asset** (menu) | — | Supplier, category, model (must match a curve file in `generalFunctions/datasets`), output flow unit, curve units. |
| **Logger** (menu) | `info`, enabled | Log level and toggle. |
| **Position** (menu) | `atEquipment` | `upstream` / `atEquipment` / `downstream` relative to parent. Icon and optional distance offset. |
> **Tip.** With `Reaction Speed = 1` and `Set 60%` from idle, the controller takes ~60 s to reach 60 %. Scale `Reaction Speed` up to emulate a faster actuator (e.g. `20` gives 1 second per 20 % = 3 s to reach 60 %).
## Input topics
Every command enters on the single input port. `msg.topic` selects the handler; `msg.payload` carries the arguments.
### `setMode`
```json
{ "topic": "setMode", "payload": "virtualControl" }
```
Valid values: `auto`, `virtualControl`, `fysicalControl`. The current mode gates *which source* may issue *which action* (mode/action/source policy lives in `generalFunctions/src/configs/rotatingMachine.json`).
### `execSequence`
```json
{ "topic": "execSequence",
"payload": { "source": "GUI", "action": "execSequence", "parameter": "startup" } }
```
`parameter` values: `startup`, `shutdown`, `entermaintenance`, `exitmaintenance`. Case is normalized.
If a `shutdown` is issued while the machine is mid-ramp (`accelerating` / `decelerating`), the active movement is aborted and the shutdown proceeds as soon as the FSM has returned to `operational`.
### `execMovement`
```json
{ "topic": "execMovement",
"payload": { "source": "GUI", "action": "execMovement", "setpoint": 60 } }
```
`setpoint` is expressed in controller units (0100 %).
### `flowMovement`
```json
{ "topic": "flowMovement",
"payload": { "source": "parent", "action": "flowMovement", "setpoint": 150 } }
```
`setpoint` is expressed in the configured **output flow unit** (e.g. m³/h). The node converts flow → controller-% via the reversed nq curve and then drives `execMovement`.
### `emergencystop`
```json
{ "topic": "emergencystop",
"payload": { "source": "GUI", "action": "emergencystop" } }
```
Aborts any active movement, runs the `emergencystop``off` transition. Allowed from every active state. Case-insensitive.
### `simulateMeasurement`
Inject a dashboard-side measurement without wiring a sensor child. Useful for validation, smoke tests, demo flows.
```json
{ "topic": "simulateMeasurement",
"payload": { "type": "pressure", "position": "upstream", "value": 200, "unit": "mbar" } }
```
`type`: `pressure` / `flow` / `temperature` / `power`. `unit` is required and must be convertible to the canonical unit for the type.
### Diagnostics
- `showWorkingCurves` — snapshot of current curve slices + computed metrics; reply on port 0.
- `CoG` — current centre-of-gravity (peak efficiency point) indicators; reply on port 0.
### `registerChild`
Internal. Sensor children (typically `measurement` nodes) send this to bind themselves to the machine. The machine also emits one on port 2 shortly after deploy so a parent group/station can register it.
## Output ports
### Port 0 — process data
Delta-compressed payload. Only *changed* fields are emitted each tick. Keys use a **4-segment** format:
```
<type>.<variant>.<position>.<childId>
```
Examples:
| Key | Meaning |
|---|---|
| `flow.predicted.downstream.default` | predicted flow at discharge |
| `flow.predicted.atequipment.default` | predicted flow at equipment |
| `power.predicted.atequipment.default` | predicted electrical power draw |
| `pressure.measured.downstream.dashboard-sim-downstream` | simulated discharge pressure |
| `pressure.measured.upstream.<childId>` | real upstream sensor reading |
| `state` | current FSM state |
| `mode` | current mode |
| `ctrl` | current controller position (0100 %) |
| `NCog` / `cog` | normalized / absolute centre-of-gravity |
| `runtime` | cumulative operational hours |
Consumers must cache and merge deltas. The example flow `01 - Basic Manual Control.json` includes a function node that does exactly this — reuse its logic in your own flows.
### Port 1 — dbase (InfluxDB)
InfluxDB line-protocol payload formatted for the `telemetry` bucket. Tags are low-cardinality fields (node name, machine type); measurements are numeric values. See the [InfluxDB Schema Design](../../concepts/influxdb-schema-design.md) page for the full tag/field contract.
### Port 2 — parent
`{ topic: "registerChild", payload: <this-node-id>, positionVsParent }` — emitted once ~180 ms after deploy so a downstream parent group can discover this machine. Subsequent commands and data flow through the parent's input port.
## State machine
```
┌────────────────────────────┐
│ operational │◄────┐
└────┬──────────┬────────┬────┘ │
│ │ │ │
execMovement │ │ │ │
execMovement │ │ │ │
▼ ▼ ▼ ▼ │
accelerating decelerating │ emergencystop ─► off
│ │ │
└─── (abort)─┘ │
│ │
┌────▼──────────▼────┐
│ stopping │
└────────┬─────────────┘
coolingdown
idle
starting
warmingup
(operational)
```
Protected states (cannot be aborted by a new command): `warmingup`, `coolingdown`.
Interruptible states: `accelerating`, `decelerating`. A `shutdown` or `emergencystop` issued during a ramp aborts the ramp and drives the FSM correctly to `idle` / `off`.
Active states (contribute to `runtime`): `operational`, `starting`, `warmingup`, `accelerating`, `decelerating`.
## Predictions and pressure
Flow and power are curve-backed. The curve set is indexed by the differential pressure across the machine:
1. Best: both upstream and downstream pressures present → real Δp.
2. Degraded: only one side present → falls back to that side with a warn.
3. Minimum: no pressure → `fDimension = 0`; flow and power predictions use the lowest curve slice and will look unrealistic.
Pressure sources are resolved in priority order **real sensor child > virtual dashboard child > aggregated fallback**. Real-child values always win.
Predictions are only emitted while the FSM is in an active state (`operational`, `starting`, `warmingup`, `accelerating`, `decelerating`). In `idle`, `stopping`, `coolingdown`, `off`, `maintenance` the outputs are clamped to zero.
### Supported curves and verification
| Model | Pressure envelope | Flow envelope | Power envelope |
|---|---|---|---|
| `hidrostal-H05K-S03R` | 700 3900 mbar (33 slices) | 9.5 227 m³/h | 8.2 65.1 kW |
| `hidrostal-C5-D03R-SHN1` | 400 2900 mbar (26 slices) | 6.4 52.5 m³/h | 0.55 31.5 kW |
Both curves are covered by unit tests (`test/integration/curve-prediction.integration.test.js`) and a live E2E benchmark (`test/e2e/curve-prediction-benchmark.py`) that sweeps each pump through its own pressure × controller envelope. Last green run: **2026-04-13** — 12/12 samples per curve inside envelope, ctrl-monotonic, inverse-pressure monotonic.
> **Pressure out of envelope is not clamped.** If a measured pressure falls *below* the curve's minimum slice, the node extrapolates and may produce implausibly large flow values (e.g. H05K at 400 mbar, ctrl 20 % → flow ≈ 30 000 m³/h; real envelope max is 227). Use realistic sensor ranges on your pressure `measurement` children.
## Example flows
In the editor: **Import ▸ Examples ▸ EVOLV ▸ rotatingMachine**.
- `01 - Basic Manual Control.json` — single machine, inject-only. Good for smoke-testing a node installation.
- `02 - Integration with Machine Group.json``machineGroupControl` with two pumps as children. Good for verifying registration and parent orchestration.
- `03 - Dashboard Visualization.json` — FlowFuse dashboard with live charts. Depends on `@flowfuse/node-red-dashboard`.
## Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Editor says `pressure not initialized`, status ring is yellow | No pressure child wired yet and no simulated pressure injected. | Inject a `simulateMeasurement` of type `pressure` (both sides preferred) or wire a `measurement` child. |
| Predictions are enormous at `ctrl = 0 %` | At near-zero controller position with high backpressure, the intercept of the curve gives a nominally-nonzero flow. This is a curve-data artefact, not a runtime bug. | Confirm the curve with Rene / supplier data. For a conservative prediction use a lower `Reaction Speed` or constrain `setpoint` ≥ 10 %. |
| "Transition aborted" / "Movement aborted" in logs | Expected during `shutdown` / `emergencystop` issued during a ramp — the fix path intentionally aborts the active move. | None — informational only. |
| Status bar shows `pressure not initialized` even after inject | `simulateMeasurement` payload missing `unit` or with a non-convertible value. | Include `unit` (e.g. `"mbar"`) and a finite number in `value`. |
| Shutdown does nothing and no error | Machine is in `warmingup` or `coolingdown` (protected). | Wait for the phase to complete (≤ configured seconds) and retry. |
## Running it locally
```bash
git clone --recurse-submodules https://gitea.wbd-rd.nl/RnD/EVOLV.git
cd EVOLV
docker compose up -d
# Node-RED: http://localhost:1880 InfluxDB: :8086 Grafana: :3000
```
Then in Node-RED: **Import ▸ Examples ▸ EVOLV ▸ rotatingMachine ▸ 01 - Basic Manual Control**.
## Testing
```bash
cd nodes/rotatingMachine
npm test
```
Unit tests (79) cover construction, mode gating, sequences, interruptible movement, emergency stop, shutdown, efficiency/CoG, pressure initialization, output formatting, listener cleanup. See also `examples/README.md` for the flow-level test matrix.
## Production status
See the project memory entry `node_rotatingMachine.md` for the latest benchmarks and wishlist. Trial-ready as of 2026-04-13 following the interruptibility + schema-sync fixes documented in [session 2026-04-13](../../sessions/2026-04-13-rotatingMachine-trial-ready.md).

@@ -1,64 +0,0 @@
---
title: Metrics Dashboard
updated: 2026-04-07
---
> **⚠️ ARCHIVED — pre-refactor (Tier 14, 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.**
# Metrics Dashboard
All numbers with provenance. Source of truth: `knowledge-graph.yaml`.
## Test Results
| Suite | Pass/Total | File | Date |
|---|---|---|---|
| rotatingMachine basic | 10/10 | test/basic/*.test.js | 2026-04-07 |
| rotatingMachine integration | 16/16 | test/integration/*.test.js | 2026-04-07 |
| rotatingMachine edge | 17/17 | test/edge/*.test.js | 2026-04-07 |
| machineGroupControl basic | 1/1 | test/basic/*.test.js | 2026-04-07 |
| machineGroupControl integration | 3/3 | test/integration/*.test.js | 2026-04-07 |
| machineGroupControl edge | 1/1 | test/edge/*.test.js | 2026-04-07 |
## Performance — machineGroupControl Optimization
| Metric | Value | Source | Date |
|---|---|---|---|
| BEP-Gravitation + refinement (3 pumps, 6 combos) | 0.027-0.153ms median | benchmark script | 2026-04-07 |
| Tick loop budget used | 0.015% of 1000ms | benchmark script | 2026-04-07 |
| Max gap from brute-force optimum (1000 steps) | 0.1% | [[BEP Gravitation Proof]] | 2026-04-07 |
| Pump switching stability (5-95% sweep) | 1-2 transitions, no hysteresis | stability sweep | 2026-04-07 |
## Performance — rotatingMachine Prediction
| Metric | Value | Source |
|---|---|---|
| predict.y(x) call | O(log n), effectively O(1) | predict_class.js |
| buildAllFxyCurves | sub-10ms for typical curves | predict_class.js |
| Curve cache | full caching of splines + calculated curves | predict_class.js |
## Power Comparison: machineGroupControl vs Baselines
Station: 2x H05K-S03R + 1x C5-D03R-SHN1 @ ΔP=2000 mbar
| Demand | Qd (m3/h) | machineGroupControl | Spillover | Equal-all | Gap to optimum |
|--------|-----------|--------------------|-----------|-----------|----|
| 10% | 71 | 17.6 kW | 22.0 kW (+25%) | 23.9 kW (+36%) | -0.10% |
| 25% | 136 | 34.6 kW | 36.3 kW (+5%) | 39.1 kW (+13%) | +0.01% |
| 50% | 243 | 62.9 kW | 73.8 kW (+17%) | 64.2 kW (+2%) | -0.00% |
| 75% | 351 | 96.8 kW | 102.9 kW (+6%) | 99.6 kW (+3%) | +0.08% |
| 90% | 415 | 122.8 kW | 123.0 kW (0%) | 123.0 kW (0%) | +0.07% |
## Disproven Claims
| Claim | Evidence For | Evidence Against | Date |
|---|---|---|---|
| NCog as proportional weight works | Simple implementation | Starves small pumps, overloads large ones at high demand | 2026-04-07 |
| Q/P ratio always has mid-range peak | Expected from pump physics | Monotonically decreasing at high ΔP due to affinity laws (P ∝ Q³) | 2026-04-07 |
| Equal-marginal-cost solver is optimal | KKT theory for convex curves | C5 curve is non-convex due to sparse data points (5 per pressure) | 2026-04-07 |

@@ -1,78 +0,0 @@
---
title: EVOLV Project Overview
created: 2026-04-07
updated: 2026-04-07
status: evolving
tags: [overview, wastewater, node-red, isa-88]
---
> **⚠️ ARCHIVED — pre-refactor (Tier 14, 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 — Edge-Layer Evolution for Optimized Virtualization
Industrial automation platform for wastewater treatment, built as custom Node-RED nodes by Waterschap Brabantse Delta R&D. Follows ISA-88 (S88) batch control standard.
## Stack
Node.js, Node-RED, InfluxDB (time-series), TensorFlow.js (prediction), CoolProp (thermodynamics). No build step — pure Node.js.
## Architecture
Each node follows a 3-tier pattern:
1. **Entry file** — registers with Node-RED, admin HTTP endpoints
2. **nodeClass** — Node-RED adapter (tick loop, message routing, status)
3. **specificClass** — pure domain logic (physics, state machines, predictions)
3-port output convention: Port 0 = process data, Port 1 = InfluxDB telemetry, Port 2 = parent-child registration.
## What Works
| Capability | Status | Evidence |
|---|---|---|
| rotatingMachine state machine | proven | 76 tests passing, all sequences verified |
| 3D pump curve prediction (flow/power from pressure+control) | proven | Monotonic cubic spline interpolation across 34 pressure levels |
| NCog / BEP tracking per pump | proven | Produces meaningful values with differential pressure |
| machineGroupControl BEP-Gravitation | proven | Within 0.1% of brute-force global optimum |
| Combination selection (2^n exhaustive) | proven | Stable: 1-2 switches across 5-95% demand sweep, no hysteresis |
| Prediction health scoring | proven | NRMSE drift, pressure source penalties, edge detection |
| Hydraulic efficiency (η = QΔP/P) | proven | CoolProp density, head calculation |
| Unit conversion chain | proven | No double-conversion, clean layer separation |
## What Doesn't Work (honestly)
| Issue | Status | Evidence |
|---|---|---|
| C5 curve non-convexity | evolving | 5 raw data points cause spline artifacts, dP/dQ non-monotonic |
| NCog = 0 at high ΔP | evolving | At ΔP > 800 mbar for H05K, Q/P is monotonically decreasing |
| calcBestCombination (NCog-weight mode) | disproven | Uses NCog as proportional weight instead of BEP target |
## Current Scale
- 13 custom Node-RED nodes (12 submodules + generalFunctions)
- rotatingMachine: 76 tests, 1563 lines domain logic
- machineGroupControl: 90+ tests, 1400+ lines domain logic
- 3 real pump curves: H05K-S03R, C5-D03R-SHN1, ECDV
- Tick loop: 1000ms interval
## Node Inventory
| Node | Purpose | Test Status |
|------|---------|-------------|
| rotatingMachine | Pump/compressor control | 76 tests (full) |
| machineGroupControl | Multi-pump optimization | 90 tests (full) |
| pumpingStation | Multi-pump station | needs review |
| valve | Valve modeling | needs review |
| valveGroupControl | Valve group coordination | needs review |
| reactor | Biological reactor (ASM kinetics) | needs review |
| settler | Secondary clarifier | needs review |
| monster | Multi-parameter bio monitoring | needs review |
| measurement | Sensor signal conditioning | needs review |
| diffuser | Aeration system control | needs review |
| dashboardAPI | InfluxDB + FlowFuse charts | needs review |
| generalFunctions | Shared utilities | partial |

@@ -1,54 +0,0 @@
---
title: "Session: Production Hardening rotatingMachine + machineGroupControl"
created: 2026-04-07
updated: 2026-04-07
status: proven
tags: [session, rotatingMachine, machineGroupControl, testing]
---
> **⚠️ ARCHIVED — pre-refactor (Tier 14, 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.**
# 2026-04-07 — Production Hardening
## Scope
Full code review and hardening of rotatingMachine and machineGroupControl nodes for production readiness.
## Key Discoveries
1. **Efficiency rounding destroyed NCog/BEP**`Math.round(Q/P * 100) / 100` in canonical units (m3/s and W) produces ratios ~1e-6 that all round to 0. All NCog, cog, and BEP calculations were non-functional. Fixed by removing rounding.
2. **flowmovement unit mismatch** — machineGroupControl computed flow in canonical (m3/s) and sent it directly to rotatingMachine which expected output units (m3/h). Every pump stayed at minimum flow. Fixed with `_canonicalToOutputFlow()`.
3. **emergencyStop case mismatch**`"emergencyStop"` vs config key `"emergencystop"`. Emergency stop never worked. Fixed to lowercase.
4. **Curve data anomalies** — 3 flow values leaked into power columns in hidrostal-H05K-S03R.json at pressures 1600, 3200, 3300 mbar. Fixed with interpolated values.
5. **C5 pump non-convexity** — 5 data points per pressure level produces non-convex spline. The marginal-cost refinement loop closes the gap to brute-force optimum from 2.1% to 0.1%.
## Changes Made
### rotatingMachine (3 files, 7 test files)
- Async input handler, null guards, listener cleanup, tick loop race fix
- showCoG() implementation, efficiency variant fix, curve anomaly detection
- 43 new tests (76 total)
### machineGroupControl (1 file, 2 test files)
- `_canonicalToOutputFlow()` on all flowmovement calls
- Absolute scaling bug, empty Qd block, empty-machines guards
- Marginal-cost refinement loop in BEP-Gravitation
- Missing flowmovement after startup in equalFlowControl
### generalFunctions (1 file)
- 3 curve data fixes in hidrostal-H05K-S03R.json
## Verification
- 90 tests passing across both nodes
- machineGroupControl within 0.1% of brute-force global optimum (1000-step search)
- Pump switching stable: 1-2 transitions across full demand range, no hysteresis
- Optimization cost: 0.03-0.15ms per call (0.015% of tick budget)

@@ -1,117 +0,0 @@
---
title: "Session: measurement node — dispatcher bug fix + digital/MQTT mode"
created: 2026-04-13
updated: 2026-04-13
status: proven
tags: [session, measurement, smoothing, outlier, mqtt, iot]
---
> **⚠️ ARCHIVED — pre-refactor (Tier 14, 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.**
# 2026-04-13 — measurement trial-ready + digital mode
## Scope
Honest review of the `measurement` node. Benchmark every method, reason about keeping the node agnostic across analog and digital sources, add a digital (MQTT/IoT) mode without breaking analog.
## Findings
### Silent dispatcher bug (critical)
`validateEnum` in `generalFunctions` lowercases enum values (`zScore``zscore`, `lowPass``lowpass`). But `specificClass.outlierDetection` and `specificClass.applySmoothing` compared against camelCase keys. Effect:
- 5 of 11 smoothing methods silently fell through to a no-op: `lowPass`, `highPass`, `weightedMovingAverage`, `bandPass`, `savitzkyGolay`.
- 2 of 3 outlier methods silently disabled: `zScore`, `modifiedZScore`.
- Only `mean`, `median`, `sd`, `min`, `max`, `none`, `kalman`, `iqr` (the already-lowercase ones) actually worked.
Users who picked any camelCase method from the dropdown got the raw last value or no outlier filtering, with no error. Flows deployed before this session that relied on these filters got no filtering at all.
### Test coverage was thin
Pre-session: **12 tests** — 1 for scaling, 1 for outlier toggle, 1 for event emit, 3 for example flow shape, 1 constructor, 1 routing, 1 invalid payload, 2 other. Every smoothing method beyond `mean` and every outlier method beyond a toggle-flip was untested. The dispatcher bug would have been caught immediately by per-method unit tests.
### Analog-only input shape
The node only accepted scalar `msg.payload`. MQTT / IoT devices commonly publish a single JSON blob with many readings per message. Every user wanting that pattern had to fan out into N measurement nodes — ugly, and the device's shared timestamp is lost.
## Fixes + additions
### Dispatcher normalization (`specificClass.js`)
Both `outlierDetection()` and `applySmoothing()` now lowercase the configured method and the lookup table keys. Legacy camelCase config values and normalized lowercase config values both work.
### `MeasurementContainer.isUnitCompatible` permissive short-circuit
Previously: if the unit couldn't be described by the convert module, compatibility returned false regardless of type. This blocked user-defined types like `humidity` with unit `%`. Now: when `measureMap[type]` is undefined (unknown type), accept any unit. Known types still validate strictly.
### Digital mode (new)
`config.mode.current === 'digital'` opts into a new input shape. `config.channels` declares one entry per JSON key. The new `Channel` class (`src/channel.js`) is a self-contained per-channel pipeline — outlier → offset → scaling → smoothing → min/max → constrain → emit. Analog behaviour is preserved exactly; flows built before this session work unchanged.
## Test additions
Before → after: **12 → 71** tests.
New files:
- `test/basic/smoothing-methods.basic.test.js` — every smoothing method covered, 16 tests.
- `test/basic/outlier-detection.basic.test.js` — every outlier method + toggle + fall-through, 10 tests.
- `test/basic/scaling-and-interpolation.basic.test.js` — offset / interpolateLinear / constrain / handleScaling / updateMinMaxValues / updateOutputPercent / updateOutputAbs / getOutput, 10 tests.
- `test/basic/calibration-and-stability.basic.test.js` — calibrate / isStable / evaluateRepeatability / toggleSimulation / tick / simulateInput, 11 tests.
- `test/integration/digital-mode.integration.test.js` — 12 tests covering channel build, payload dispatch, multi-channel emit, unknown keys, per-channel scaling / smoothing / outlier, empty channels, malformed entries, non-numeric values, digital-output shape.
## E2E verification (Dockerized Node-RED)
### Analog baseline — `/tmp/m_e2e_baseline.py`
Deploys `examples/basic.flow.json`, fires `{topic:"measurement", payload:42}` repeatedly. Observed port-0 output: `mAbs` climbed 0 → 2.1 → 2.8 → 3.15 → 3.36 → 4.2 across five ticks as the mean window filled with 42s (scaling 0..100 → 0..10). Tick cadence 9091001 ms (avg 981 ms). Registration at t=0.22 s.
### Digital end-to-end — `/tmp/m_digital_e2e.py`
Deploys a single measurement node in digital mode with three channels (`temperature` / `humidity` / `pressure`) and fires two MQTT-shaped payloads.
| Tick | Channel | mAbs | totalMinSmooth | totalMaxSmooth |
|---|---|---:|---:|---:|
| after inject 1 | temperature | 22.5 | 22.5 | 22.5 |
| after inject 1 | humidity | 45 | 45 | 45 |
| after inject 1 | pressure | 1013 | 1013 | 1013 |
| after inject 2 | temperature | 24 | 22.5 | 24 |
| after inject 2 | humidity | 42.5 | 42.5 | 45 |
| after inject 2 | pressure | 1014 | 1013 | 1014 |
Mean smoothing across a window of 3 computed per-channel, the `unknown` key in the payload ignored, all three events emitted on `<type>.measured.atequipment`.
## Files changed
```
nodes/generalFunctions/src/measurements/MeasurementContainer.js # permissive unit check for user-defined types
nodes/generalFunctions/src/configs/measurement.json # mode + channels schema
nodes/measurement/src/channel.js # new per-channel pipeline class
nodes/measurement/src/specificClass.js # dispatcher fix + digital dispatch
nodes/measurement/src/nodeClass.js # mode-aware input handler + tick
nodes/measurement/measurement.html # Mode dropdown + Channels JSON + help panel
nodes/measurement/README.md # rewrite
nodes/measurement/test/basic/smoothing-methods.basic.test.js # +16 tests
nodes/measurement/test/basic/outlier-detection.basic.test.js # +10 tests
nodes/measurement/test/basic/scaling-and-interpolation.basic.test.js # +10 tests
nodes/measurement/test/basic/calibration-and-stability.basic.test.js # +11 tests
nodes/measurement/test/integration/digital-mode.integration.test.js # +12 tests
```
## Production status
Trial-ready for both modes. Supervised trial recommended for digital-mode deployments until the channels-editor UI (currently a JSON textarea) lands.
## Follow-ups
- Repeatable-row editor widget for channels.
- `validateArray.minLength=0` evaluates as falsy; pre-existing generalFunctions bug affecting this node's `channels` and also `measurement.assetRegistration.childAssets`. Harmless warn at deploy time.
- Per-channel calibration + simulation for digital mode.
- Runtime channel reconfiguration via a dedicated topic (`addChannel` / `removeChannel`).

@@ -1,142 +0,0 @@
---
title: "Session: rotatingMachine trial-ready — FSM interruptibility, config schema, UX fixes"
created: 2026-04-13
updated: 2026-04-13
status: proven
tags: [session, rotatingMachine, state-machine, docker, e2e]
---
> **⚠️ ARCHIVED — pre-refactor (Tier 14, 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.**
# 2026-04-13 — rotatingMachine trial-ready
## Scope
Honest review + production-hardening pass on `rotatingMachine`. Fixes landed on top of the 2026-04-07 hardening and are verified against a Docker-hosted Node-RED stack.
## Findings (before fixes)
From a live E2E run captured via the Node-RED debug websocket (`/comms`):
- **Clean startup→operational→shutdown→idle path** works to spec: 3 s starting + 2 s warmup + 3 s stopping + 2 s cooldown, matching config exactly.
- **Tick cadence:** 1000 ms (min 1000, max 1005, avg 1002.5).
- **Predictions** gate correctly on pressure injection; at 900 mbar Δp the hidrostal-H05K-S03R curve yields a monotonic flow/power response.
- **State machine FSM** *rejects* `stopping`/`coolingdown`/`idle` transitions while the machine is in `accelerating`/`decelerating`, leaving a shutdown command silently dropped. Log symptom: `Invalid transition from accelerating to stopping. Transition not executed.`
- **Sequence `emergencyStop` not defined** warn appears when a parent orchestrator with the capital-S casing (e.g. `machineGroupControl` config) forwards the sequence name.
- **Config validator strips** `functionality.distance` and top-level `output` that `buildConfig` adds; every deploy prints removal warnings.
- Cosmetic: typo "acurate" in single-side pressure warn; editor lacks unit hints for `speed` / `startup` / etc.
## Fixes
### 1. Interruptible movement (`generalFunctions/src/state/state.js`)
`moveTo`'s `catch` block now detects `Movement aborted` / `Transition aborted` errors and transitions the FSM back to `operational`, unblocking subsequent sequence transitions. A new `movementAborted` event is emitted for observability.
### 2. Auto-abort on shutdown/emergency-stop (`rotatingMachine/src/specificClass.js`)
`executeSequence` now:
- Normalizes the sequence name to lowercase (defensive against parent callers using mixed case).
- When `shutdown` or `emergencystop` is requested from `accelerating`/`decelerating`, calls `state.abortCurrentMovement(...)` and waits up to 2 s for the FSM to return to `operational` via the new `_waitForOperational(timeoutMs)` helper that listens on the state emitter.
### 3. Config schema sync (`generalFunctions/src/configs/rotatingMachine.json`)
Added to the schema:
- `functionality.distance`, `.distanceUnit`, `.distanceDescription` (produced by the HTML editor).
- Top-level `output.process` / `output.dbase` (produced by `buildConfig`).
Also reverted an overly broad `buildConfig` addition to only emit `distance` (not `distanceUnit`/`distanceDescription`) so other nodes aren't forced to add these to their schemas.
### 4. UX polish
- Fixed typo "acurate" → "accurate" in the single-side pressure warning, plus made the message actionable.
- Added unit hints to Reaction Speed / Startup / Warmup / Shutdown / Cooldown fields in the editor.
- Expanded the Node-RED help panel with a topic reference, state diagram, prediction rules, and port documentation.
## Tests added
`test/integration/interruptible-movement.integration.test.js` — three regression tests for the FSM fix:
- `shutdown during accelerating aborts the move and reaches idle`
- `emergency stop during accelerating reaches off`
- `executeSequence accepts mixed-case sequence names`
`test/integration/curve-prediction.integration.test.js` — 12 parametrized tests across both shipped pump curves (`hidrostal-H05K-S03R` and `hidrostal-C5-D03R-SHN1`):
- Curve loader returns nq + np with matching pressure slices.
- Predicted flow and power at mid-pressure / mid-ctrl are finite and inside the curve envelope.
- Flow is monotonically non-decreasing across a ctrl sweep at fixed pressure.
- Flow decreases (or stays level) when pressure rises at fixed ctrl — centrifugal-pump physics.
- CoG / NCog are computed, finite, and inside [0, 100] controller units.
- Reverse predictor (flow → ctrl via reversed nq) round-trips within 10 % of the known controller position.
`test/e2e/curve-prediction-benchmark.py` + `test/e2e/README.md` — live Dockerized Node-RED benchmark that deploys one rotatingMachine per curve and records a (pressure × ctrl) sweep.
Full unit suite: **91/91 passing** (was 76/76 on the morning review).
## E2E verification (Dockerized Node-RED)
Via `/tmp/rm_e2e_verify.py` — deploys the example flow to `docker compose`-hosted Node-RED, drives it via `POST /inject/:id`, captures port-output via `ws://localhost:1880/comms`.
| Scenario | Observed state sequence | Pass? |
|---|---|---|
| Shutdown fired while `accelerating` | starting → warmingup → operational → accelerating → decelerating → stopping → coolingdown → **idle** | ✅ |
| Emergency stop fired while `accelerating` | starting → warmingup → operational → accelerating → **off** | ✅ |
| Clean startup → shutdown (regression) | starting → warmingup → operational → stopping → coolingdown → idle | ✅ |
Container log scan over a 3-minute window:
- `Unknown key` warns: 0 (was 6+ per deploy)
- `acurate` typo: 0 (was 2)
- `Invalid transition from accelerating/decelerating to ...` errors: 0 (was 3+)
- `Sequence '...' not defined`: 0 (was 1)
### Dual-curve prediction sweep
Via `nodes/rotatingMachine/test/e2e/curve-prediction-benchmark.py`. Deploys two live rotatingMachines, one per pump curve, and runs a (pressure × ctrl) sweep per pump. Each pump is tested only inside its own curve envelope.
| Pump | Pressures swept (mbar) | Ctrl setpoints (%) | Samples in envelope | Flow monotonic | Flow observed (m³/h) | Power observed (kW) |
|---|---|---|---|---|---|---|
| hidrostal-H05K-S03R | 700 / 2300 / 3900 | 20 / 40 / 60 / 80 | 12/12 ✅ | ✅ | 10.3 208.3 | 12.3 50.3 |
| hidrostal-C5-D03R-SHN1 | 400 / 1700 / 2900 | 20 / 40 / 60 / 80 | 12/12 ✅ | ✅ | 8.7 45.6 | 0.7 13.0 |
Inverse-pressure monotonicity (centrifugal-pump physics) also verified: for both pumps, flow at the highest pressure slice is strictly lower than flow at the lowest pressure slice for the same ctrl.
**Known limitation** captured in the memory file: extrapolating pressure *below* the curve's minimum slice produces nonsensical flow values (e.g. H05K at 400 mbar ctrl=20% predicts ~30 000 m³/h vs envelope max 227 m³/h). Upstream `measurement` nodes are expected to clamp sensors to realistic ranges; rotatingMachine itself does not.
Separately, the C5 curve still exhibits the previously-documented power non-monotonicity at p=1700 mbar (sparse-data spline artefact noted in the 2026-04-07 session); this is compensated by the group-optimization marginal-cost refinement loop.
## Files changed
```
nodes/generalFunctions/src/state/state.js # abort recovery
nodes/generalFunctions/src/configs/index.js # buildConfig trim
nodes/generalFunctions/src/configs/rotatingMachine.json # schema sync
nodes/rotatingMachine/src/specificClass.js # exec + typo
nodes/rotatingMachine/rotatingMachine.html # UX hints + help
nodes/rotatingMachine/test/integration/interruptible-movement.integration.test.js # +3 tests (FSM)
nodes/rotatingMachine/test/integration/curve-prediction.integration.test.js # +12 tests (dual curve)
nodes/rotatingMachine/test/e2e/curve-prediction-benchmark.py # new E2E benchmark
nodes/rotatingMachine/test/e2e/README.md # benchmark docs
nodes/rotatingMachine/README.md # rewrite
```
## Production readiness
Status: **trial-ready**. The caveats flagged in the 2026-04-13 memory file (`node_rotatingMachine.md`) are resolved. Remaining items are in the wishlist (interruptible curve validation feedback, domain review of ctrl≈0% + backpressure flow prediction, opt-in full-snapshot port-0 mode, per-machine `/health` endpoint).
## Verification command
```bash
cd /mnt/d/gitea/EVOLV
docker compose up -d nodered influxdb
cd nodes/rotatingMachine && npm test
python3 /tmp/rm_e2e_verify.py # end-to-end smoke
```

@@ -1,59 +1,64 @@
# Archive — pre-refactor wiki pages # Archive
Pages kept for historical reference. **Do not update them.** Corrections go on the current page; if you find a meaningful inaccuracy in an archived page, leave it and add a note to the *current* page explaining what changed. > [!NOTE]
> Pre-refactor wiki content has been removed from the live wiki. The git history of `EVOLV.wiki.git` preserves every prior page if you need to consult it. This page lists what was removed and when, plus where to look for the current content.
Two batches of archived material: ---
## Live-wiki pages (originally on `EVOLV.wiki.git` before 2026-05-11) ## What was removed (2026-05-11)
These pages were on the live Gitea wiki before the 2026-05-11 refactor wave. They have been renamed with the `Archive-` prefix and stamped with the archive banner. The 2026-05-11 wiki refactor removed nine pre-refactor pages from the live `EVOLV.wiki.git`:
| Page | Era | Archived on | | Removed page | Era | Successor |
|---|---|---| |:---|:---|:---|
| [Architecture: Configuration Model & Tagcodering](Archive-Architecture-Configuration-Model-and-Tagcodering) | Pre-refactor planning doc | 2026-05-11 | | `Architecture-Configuration-Model-and-Tagcodering` | Pre-refactor planning doc | [Architecture](Architecture) |
| [Architecture: Container Topology](Archive-Architecture-Container-Topology) | Pre-refactor Docker / container planning | 2026-05-11 | | `Architecture-Container-Topology` | Pre-refactor Docker / container planning | [Architecture](Architecture), [Getting Started](Getting-Started) |
| [Architecture: Deployment Blueprint](Archive-Architecture-Deployment-Blueprint) | Pre-refactor rollout plan | 2026-05-11 | | `Architecture-Deployment-Blueprint` | Pre-refactor rollout plan | [Architecture](Architecture), [Getting Started](Getting-Started) |
| [Architecture: Deployment Controls Checklist](Archive-Architecture-Deployment-Controls-Checklist) | Pre-refactor go/no-go checklist | 2026-05-11 | | `Architecture-Deployment-Controls-Checklist` | Pre-refactor go / no-go checklist | (none &mdash; superseded by per-node `wiki/Home.md`) |
| [Architecture: Platform Overview](Archive-Architecture-Platform-Overview) | Pre-refactor edge/site/central layering | 2026-05-11 | | `Architecture-Platform-Overview` | Pre-refactor edge / site / central layering | [Home](Home), [Architecture](Architecture) |
| [Architecture: Security & Access Boundaries](Archive-Architecture-Security-and-Access-Boundaries) | Pre-refactor security model | 2026-05-11 | | `Architecture-Security-and-Access-Boundaries` | Pre-refactor security model | [OT Security IEC 62443](Concept-OT-Security-IEC62443) |
| [Architecture: Security & Regulatory Mapping](Archive-Architecture-Security-and-Regulatory-Mapping) | Pre-refactor IEC 62443 mapping notes | 2026-05-11 | | `Architecture-Security-and-Regulatory-Mapping` | Pre-refactor IEC 62443 mapping | [OT Security IEC 62443](Concept-OT-Security-IEC62443) |
| [Architecture: Telemetry & Smart Storage](Archive-Architecture-Telemetry-and-Smart-Storage) | Pre-refactor telemetry blueprint | 2026-05-11 | | `Architecture-Telemetry-and-Smart-Storage` | Pre-refactor telemetry blueprint | [Telemetry](Telemetry), [InfluxDB Schema Design](Concept-InfluxDB-Schema-Design) |
| [AI-Assisted Coding](Archive-AI-assisted-coding.-) | Pre-refactor coding-with-AI usage note | 2026-05-11 | | `AI-assisted coding.-` | Pre-refactor coding-with-AI usage note | (none) |
## Source-tree pages (originally under `EVOLV/wiki/` before 2026-05-11) Plus, in the source tree under `EVOLV/wiki/`, the audit moved a further 20 stale pages to `wiki/Archive/<name>.md` (these were never published to the live wiki, only existed in the source repo).
These were under the EVOLV source repo's `wiki/` directory and were archived during the audit. They have been renamed with the `Archive-Source-` prefix. ---
| Page | Era | Archived on | ## How to recover an archived page
|---|---|---|
| [SCHEMA](Archive-Source-SCHEMA) | Pre-refactor wiki maintenance schema (Obsidian-style) | 2026-05-11 | The content has not been lost &mdash; git keeps it.
| [Wiki index](Archive-Source-index) | Pre-refactor wiki index (2026-04-13) | 2026-05-11 |
| [Wiki log](Archive-Source-log) | Pre-refactor session log, single Apr-07 entry | 2026-05-11 | ```bash
| [Metrics dashboard](Archive-Source-metrics) | Pre-refactor test counts (Apr-07 snapshot, 43 tests) | 2026-05-11 | git clone https://gitea.wbd-rd.nl/RnD/EVOLV.wiki.git
| [Project overview](Archive-Source-overview) | Pre-refactor node inventory ("needs review" for most nodes) | 2026-05-11 | cd EVOLV.wiki
| [Architecture: Node architecture](Archive-Source-architecture-node-architecture) | Pre-refactor 3-tier diagram with old `_loadConfig` internals | 2026-05-11 | git log --all --diff-filter=D --name-only | grep '<page-name>'
| [Architecture: Platform overview](Archive-Source-architecture-platform-overview) | Pre-refactor edge/site/central vision | 2026-05-11 | git show <commit>:<path>
| [Architecture: Stack review](Archive-Source-architecture-stack-review) | Pre-refactor full stack analysis | 2026-05-11 | ```
| [Architecture: 3D pump curves](Archive-Source-architecture-3d-pump-curves) | Pre-refactor predict_class internals | 2026-05-11 |
| [Architecture: Group optimization](Archive-Source-architecture-group-optimization) | Pre-refactor BEP-Gravitation walkthrough | 2026-05-11 | Or use Gitea's web UI:
| [Architecture: Deployment blueprint](Archive-Source-architecture-deployment-blueprint) | Pre-refactor Docker topology | 2026-05-11 |
| [Concepts: generalFunctions API](Archive-Source-concepts-generalfunctions-api) | Pre-refactor API ref — missing BaseDomain / BaseNodeAdapter / ChildRouter | 2026-05-11 | 1. Open https://gitea.wbd-rd.nl/RnD/EVOLV/commits/branch/main
| [Concepts: Sources readme](Archive-Source-concepts-sources-readme) | Empty placeholder | 2026-05-11 | 2. Click into the 2026-05-11 refactor commit
| [Findings: Open issues 2026-03](Archive-Source-findings-open-issues-2026-03) | Issues 15 resolved by refactor | 2026-05-11 | 3. Scroll to the deleted files
| [Session: 2026-04-07 production hardening](Archive-Source-sessions-2026-04-07-production-hardening) | rotatingMachine + MGC session log | 2026-05-11 |
| [Session: 2026-04-13 rotatingMachine trial-ready](Archive-Source-sessions-2026-04-13-rotatingMachine-trial-ready) | FSM interruptibility session log | 2026-05-11 | ---
| [Session: 2026-04-13 measurement digital mode](Archive-Source-sessions-2026-04-13-measurement-digital-mode) | Dispatcher fix + digital mode session log | 2026-05-11 |
| [Manual: rotatingMachine (pre-refactor)](Archive-Source-manuals-nodes-rotatingMachine) | Superseded by [per-repo wiki](https://gitea.wbd-rd.nl/RnD/rotatingMachine/wiki/Home) | 2026-05-11 |
| [Manual: measurement (pre-refactor)](Archive-Source-manuals-nodes-measurement) | Superseded by [per-repo wiki](https://gitea.wbd-rd.nl/RnD/measurement/wiki/Home) | 2026-05-11 |
| [Knowledge graph YAML](Archive-Source-knowledge-graph) | Apr-07 test / metrics snapshot | 2026-05-11 |
## Where to look instead ## Where to look instead
| For | See | | For ... | See |
|---|---| |:---|:---|
| Top-level navigation | [Home](Home) | | Top-level navigation | [Home](Home) |
| Code architecture (BaseDomain / 3-tier / generalFunctions) | [Architecture](Architecture) | | Code architecture (BaseDomain / three-tier / generalFunctions) | [Architecture](Architecture) |
| Typical plant configurations | [Topology-Patterns](Topology-Patterns) | | Typical plant configurations | [Topology Patterns](Topology-Patterns) |
| Topic naming + units + S88 colours | [Topic-Conventions](Topic-Conventions) | | Topic naming, units, S88 colours | [Topic Conventions](Topic-Conventions) |
| Port 0/1/2 + InfluxDB schema + Grafana | [Telemetry](Telemetry) | | Port 0 / 1 / 2, InfluxDB schema, Grafana | [Telemetry](Telemetry) |
| Per-node operator reference | The node's own wiki on `gitea.wbd-rd.nl/RnD/<node>/wiki` | | Per-node operator reference | The node's own wiki on `gitea.wbd-rd.nl/RnD/<node>/wiki` |
---
## Related pages
| Page | Why |
|:---|:---|
| [Home](Home) | Current top-level navigation |

@@ -1,5 +1,11 @@
# Activated Sludge Models (ASM1, ASM2d, ASM3) # Activated Sludge Models (ASM1, ASM2d, ASM3)
![code-ref](https://img.shields.io/badge/code--ref-9ab9f6b-blue) ![type](https://img.shields.io/badge/type-domain_concept-orange)
> [!NOTE]
> Reference page. Maintained for context; not regenerated by code. See [Home](Home) for current top-level navigation.
> **Used by**: `biological-process-engineer` agent, `reactor` node, `monster` node > **Used by**: `biological-process-engineer` agent, `reactor` node, `monster` node
> **Validation**: Verified against IWA publications, WaterTAP documentation, and peer-reviewed literature > **Validation**: Verified against IWA publications, WaterTAP documentation, and peer-reviewed literature

@@ -1,5 +1,11 @@
# InfluxDB Time-Series Best Practices # InfluxDB Time-Series Best Practices
![code-ref](https://img.shields.io/badge/code--ref-9ab9f6b-blue) ![type](https://img.shields.io/badge/type-domain_concept-orange)
> [!NOTE]
> Reference page. Maintained for context; not regenerated by code. See [Home](Home) for current top-level navigation.
> **Used by**: `telemetry-database` agent, `dashboardAPI` node > **Used by**: `telemetry-database` agent, `dashboardAPI` node
> **Validation**: Verified against InfluxDB official documentation (v1, v2, v3) > **Validation**: Verified against InfluxDB official documentation (v1, v2, v3)

@@ -1,5 +1,11 @@
# OT Security Standards — IEC 62443 & NIST SP 800-82 # OT Security Standards — IEC 62443 & NIST SP 800-82
![code-ref](https://img.shields.io/badge/code--ref-9ab9f6b-blue) ![type](https://img.shields.io/badge/type-domain_concept-orange)
> [!NOTE]
> Reference page. Maintained for context; not regenerated by code. See [Home](Home) for current top-level navigation.
> **Used by**: `ot-security-integration` agent > **Used by**: `ot-security-integration` agent
> **Validation**: Verified against IEC 62443 series, NIST SP 800-82, Dragos, and Rockwell Automation publications > **Validation**: Verified against IEC 62443 series, NIST SP 800-82, Dragos, and Rockwell Automation publications

@@ -1,5 +1,11 @@
# PID Control for Process Applications # PID Control for Process Applications
![code-ref](https://img.shields.io/badge/code--ref-9ab9f6b-blue) ![type](https://img.shields.io/badge/type-domain_concept-orange)
> [!NOTE]
> Reference page. Maintained for context; not regenerated by code. See [Home](Home) for current top-level navigation.
> **Used by**: `mechanical-process-engineer` agent, `node-red-runtime` agent, `generalFunctions/src/pid/` > **Used by**: `mechanical-process-engineer` agent, `node-red-runtime` agent, `generalFunctions/src/pid/`
> **Validation**: Verified against Astrom & Hagglund (ISA, 2006) and MATLAB/Simulink documentation > **Validation**: Verified against Astrom & Hagglund (ISA, 2006) and MATLAB/Simulink documentation

@@ -1,5 +1,11 @@
# Pump Affinity Laws & Curve Theory # Pump Affinity Laws & Curve Theory
![code-ref](https://img.shields.io/badge/code--ref-9ab9f6b-blue) ![type](https://img.shields.io/badge/type-domain_concept-orange)
> [!NOTE]
> Reference page. Maintained for context; not regenerated by code. See [Home](Home) for current top-level navigation.
> **Used by**: `mechanical-process-engineer` agent, `rotatingMachine` node, `pumpingStation` node > **Used by**: `mechanical-process-engineer` agent, `rotatingMachine` node, `pumpingStation` node
> **Validation**: Verified against Engineering Toolbox, Hydraulic Institute standards, and ScienceDirect > **Validation**: Verified against Engineering Toolbox, Hydraulic Institute standards, and ScienceDirect

@@ -1,5 +1,11 @@
# Sludge Settling & Clarifier Models # Sludge Settling & Clarifier Models
![code-ref](https://img.shields.io/badge/code--ref-9ab9f6b-blue) ![type](https://img.shields.io/badge/type-domain_concept-orange)
> [!NOTE]
> Reference page. Maintained for context; not regenerated by code. See [Home](Home) for current top-level navigation.
> **Used by**: `biological-process-engineer` agent, `settler` node > **Used by**: `biological-process-engineer` agent, `settler` node
> **Validation**: Verified against Takacs et al. (1991), Vesilind (1968), and Burger-Diehl framework publications > **Validation**: Verified against Takacs et al. (1991), Vesilind (1968), and Burger-Diehl framework publications

@@ -1,5 +1,11 @@
# Sensor Signal Conditioning & Data Quality # Sensor Signal Conditioning & Data Quality
![code-ref](https://img.shields.io/badge/code--ref-9ab9f6b-blue) ![type](https://img.shields.io/badge/type-domain_concept-orange)
> [!NOTE]
> Reference page. Maintained for context; not regenerated by code. See [Home](Home) for current top-level navigation.
> **Used by**: `instrumentation-measurement` agent, `measurement` node > **Used by**: `instrumentation-measurement` agent, `measurement` node
> **Validation**: Verified against IEC 61298, sensor manufacturer literature, and signal processing references > **Validation**: Verified against IEC 61298, sensor manufacturer literature, and signal processing references

@@ -1,5 +1,11 @@
# Dutch Wastewater Regulations & Compliance # Dutch Wastewater Regulations & Compliance
![code-ref](https://img.shields.io/badge/code--ref-9ab9f6b-blue) ![type](https://img.shields.io/badge/type-domain_concept-orange)
> [!NOTE]
> Reference page. Maintained for context; not regenerated by code. See [Home](Home) for current top-level navigation.
> **Used by**: `commissioning-compliance` agent, `biological-process-engineer` agent > **Used by**: `commissioning-compliance` agent, `biological-process-engineer` agent
> **Validation**: Verified against EU Directive 91/271/EEC, Activiteitenbesluit milieubeheer, and Dutch water authority publications > **Validation**: Verified against EU Directive 91/271/EEC, Activiteitenbesluit milieubeheer, and Dutch water authority publications

@@ -7,6 +7,11 @@ tags: [machineGroupControl, optimization, BEP, brute-force]
sources: [nodes/machineGroupControl/test/integration/distribution-power-table.integration.test.js] sources: [nodes/machineGroupControl/test/integration/distribution-power-table.integration.test.js]
--- ---
![code-ref](https://img.shields.io/badge/code--ref-9ab9f6b-blue) ![type](https://img.shields.io/badge/type-algorithm_finding-orange)
> [!NOTE]
> Reference page. Maintained for context; not regenerated by code. See [Home](Home) for current top-level navigation.
# BEP-Gravitation vs Brute-Force Global Optimum # BEP-Gravitation vs Brute-Force Global Optimum
## Claim ## Claim

@@ -7,6 +7,11 @@ tags: [curves, interpolation, C5, non-convex]
sources: [nodes/generalFunctions/datasets/assetData/curves/hidrostal-C5-D03R-SHN1.json] sources: [nodes/generalFunctions/datasets/assetData/curves/hidrostal-C5-D03R-SHN1.json]
--- ---
![code-ref](https://img.shields.io/badge/code--ref-9ab9f6b-blue) ![type](https://img.shields.io/badge/type-algorithm_finding-orange)
> [!NOTE]
> Reference page. Maintained for context; not regenerated by code. See [Home](Home) for current top-level navigation.
# Pump Curve Non-Convexity from Sparse Data # Pump Curve Non-Convexity from Sparse Data
## Finding ## Finding

@@ -7,6 +7,11 @@ tags: [rotatingMachine, NCog, BEP, efficiency]
sources: [nodes/rotatingMachine/src/specificClass.js] sources: [nodes/rotatingMachine/src/specificClass.js]
--- ---
![code-ref](https://img.shields.io/badge/code--ref-9ab9f6b-blue) ![type](https://img.shields.io/badge/type-algorithm_finding-orange)
> [!NOTE]
> Reference page. Maintained for context; not regenerated by code. See [Home](Home) for current top-level navigation.
# NCog — Normalized Center of Gravity # NCog — Normalized Center of Gravity
## What It Is ## What It Is

@@ -7,6 +7,11 @@ tags: [machineGroupControl, stability, switching]
sources: [nodes/machineGroupControl/test/integration/ncog-distribution.integration.test.js] sources: [nodes/machineGroupControl/test/integration/ncog-distribution.integration.test.js]
--- ---
![code-ref](https://img.shields.io/badge/code--ref-9ab9f6b-blue) ![type](https://img.shields.io/badge/type-algorithm_finding-orange)
> [!NOTE]
> Reference page. Maintained for context; not regenerated by code. See [Home](Home) for current top-level navigation.
# Pump Switching Stability # Pump Switching Stability
## Concern ## Concern

@@ -1,20 +1,26 @@
# Getting Started # Getting Started
> **Reflects code as of `9ab9f6b` · regenerated `2026-05-11`** ![code-ref](https://img.shields.io/badge/code--ref-9ab9f6b-blue)
![platform-tests](https://img.shields.io/badge/platform_tests-823%2F823-brightgreen)
How to clone, install, and run EVOLV locally; how to deploy the example flows; and where to go next. > [!NOTE]
> Clone the repo with `--recurse-submodules`, install dependencies, then either spin up the Docker stack (recommended) or symlink into a local Node-RED. Import an example flow, deploy, watch the state machine run. Total time is around ten minutes.
---
## Prerequisites ## Prerequisites
| Tool | Version | Why | | Tool | Required version | Why |
|---|---|---| |:---|:---|:---|
| Node.js | 18 LTS | Node-RED 4 requires 18+ | | Node.js | 18 LTS or newer | Node-RED 4 requires 18+ |
| npm | ≥ 9 | Comes with Node.js | | npm | 9 or newer | Bundled with Node.js |
| git | 2.35 | Submodule support | | git | 2.35 or newer | Submodule support |
| Docker + compose v2 | optional | For the local Node-RED + InfluxDB stack | | Docker + compose v2 | Optional | Local Node-RED + InfluxDB stack |
| WSL2 (on Windows) | optional | Recommended for native docker performance | | WSL2 (Windows) | Optional | Recommended for native Docker performance |
## Clone and install ---
## Step 1 &mdash; Clone and install
```bash ```bash
git clone --recurse-submodules https://gitea.wbd-rd.nl/RnD/EVOLV.git git clone --recurse-submodules https://gitea.wbd-rd.nl/RnD/EVOLV.git
@@ -22,62 +28,70 @@ cd EVOLV
npm install npm install
``` ```
If you cloned without `--recurse-submodules`, run: If you forgot the recurse flag:
```bash ```bash
git submodule update --init --recursive git submodule update --init --recursive
``` ```
There are 12 submodules — one per node (`generalFunctions` plus 11 active nodes). Each lives under `nodes/<name>/`. There are 12 submodules &mdash; `generalFunctions` plus 11 active nodes. Each lives under `nodes/<name>/`.
## Verify the platform builds ---
## Step 2 &mdash; Verify the platform builds
```bash ```bash
npm run test:platform # expect 823 / 0 npm run test:platform
``` ```
This runs the full unit test suite across all 12 submodules. ~3 minutes on a workstation (reactor's mathjs initialisation dominates). Expect: `823 / 0` (823 passing, 0 failing) across all 12 submodules. Around three minutes on a workstation; reactor's mathjs initialisation dominates.
## Option A — Run via Docker (recommended) > [!TIP]
> A failure here usually means a submodule isn't on its `development` tip or `npm install` didn't complete. Run `git submodule update --init --recursive` and try again.
The repo ships a `docker-compose.yml` that brings up Node-RED + InfluxDB pre-loaded with the EVOLV nodes: ---
## Step 3 &mdash; Pick a run mode
### Option A &mdash; Docker (recommended)
```bash ```bash
docker compose up -d docker compose up -d
``` ```
When healthy: Brings up Node-RED + InfluxDB pre-loaded with EVOLV nodes.
| Service | URL | | Service | URL |
|---|---| |:---|:---|
| Node-RED editor | http://localhost:1880 | | Node-RED editor | http://localhost:1880 |
| FlowFuse dashboard (if widgets installed) | http://localhost:1880/dashboard | | FlowFuse dashboard | http://localhost:1880/dashboard |
| InfluxDB UI | http://localhost:8086 | | InfluxDB UI | http://localhost:8086 |
Watch the container logs while you click around: Watch logs:
```bash ```bash
docker compose logs -f nodered docker compose logs -f nodered
``` ```
**WSL2 note:** use the native `docker` from Ubuntu, not `docker.exe` from Windows Docker Desktop. systemd `docker.service` should be enabled and your user in the `docker` group. Compose v2 plugin lives at `~/.docker/cli-plugins/docker-compose`. > [!WARNING]
> WSL2 users: use the native Ubuntu `docker`, not `docker.exe` from Windows Docker Desktop. The compose v2 plugin lives at `~/.docker/cli-plugins/docker-compose`.
## Option B — Run via local Node-RED ### Option B &mdash; Local Node-RED
If you already have Node-RED installed in `~/.node-red`: If you already have Node-RED installed:
```bash ```bash
# in EVOLV/ # from EVOLV/
ln -s "$PWD" ~/.node-red/nodes/EVOLV ln -s "$PWD" ~/.node-red/nodes/EVOLV
node-red
``` ```
Or add to your `~/.node-red/settings.js`: Or via `~/.node-red/settings.js`:
```js ```js
module.exports = { module.exports = {
// ...
nodesDir: ['/path/to/EVOLV/nodes'], nodesDir: ['/path/to/EVOLV/nodes'],
} };
``` ```
Then start Node-RED: Then start Node-RED:
@@ -86,33 +100,40 @@ Then start Node-RED:
node-red node-red
``` ```
## Your first flow ---
Each node ships with example flows under `nodes/<name>/examples/`. The recommended starting point is **rotatingMachine — Basic Manual Control**: ## Step 4 &mdash; Run your first flow
Every node ships example flows under `nodes/<name>/examples/`. The recommended start is the rotatingMachine "Basic Manual Control" example.
```bash ```bash
# Copy the example into your Node-RED user dir
cp nodes/rotatingMachine/examples/01-Basic-Manual-Control.json ~/.node-red/ cp nodes/rotatingMachine/examples/01-Basic-Manual-Control.json ~/.node-red/
``` ```
In the editor: In the Node-RED editor:
1. Menu **Import** → select the file **Import**. 1. Menu &rarr; Import &rarr; pick the file &rarr; Import.
2. Hit **Deploy**. 2. Click Deploy.
3. Open the dashboard at http://localhost:1880/dashboard. 3. Open http://localhost:1880/dashboard.
4. Click the **startup** button. Watch the state machine progress: `idle → starting → warmingup → operational`. 4. Click the startup button.
5. Drag the demand slider. The flow + power predictions update in real time. 5. Watch the state machine progress: `idle` &rarr; `starting` &rarr; `warmingup` &rarr; `operational`.
6. Drag the demand slider. Flow + power predictions update in real time.
## What to read next > [!TIP]
> Inject pressure for meaningful predictions. Without pressure data, `fDimension=0` produces unrealistic flow/power values. The example flow injects a simulated pressure profile.
---
## Step 5 &mdash; What to read next
```mermaid ```mermaid
flowchart TB flowchart LR
start[You are here]:::neutral start["You are here"]
arch[Architecture<br/>3-tier code structure]:::tier1 arch["Architecture &mdash; 3-tier code"]
topo[Topology-Patterns<br/>typical plant configs]:::tier1 topo["Topology Patterns &mdash; plant configs"]
conv[Topic-Conventions<br/>naming + units]:::tier1 node["Pick a node &mdash; per-repo wiki"]
tele[Telemetry<br/>Port 0/1/2 + InfluxDB]:::tier1 conv["Topic Conventions &mdash; naming + units"]
node[Pick a node's wiki<br/>per-repo Home.md]:::tier3 tele["Telemetry &mdash; Port 0/1/2 + InfluxDB"]
start --> arch start --> arch
start --> topo start --> topo
@@ -121,52 +142,65 @@ flowchart TB
node --> conv node --> conv
node --> tele node --> tele
class start neutral
class arch,topo,conv,tele step
class node domain
classDef neutral fill:#dddddd classDef neutral fill:#dddddd
classDef tier1 fill:#a9daee,color:#000 classDef step fill:#a9daee,color:#000
classDef tier3 fill:#50a8d9,color:#000 classDef domain fill:#50a8d9,color:#000
``` ```
| Path | Why | | Path | Why |
|---|---| |:---|:---|
| [Architecture](Architecture) | Internalise the 3-tier (entry nodeClass specificClass) pattern. | | [Architecture](Architecture) | Internalise the three-tier (entry &rarr; nodeClass &rarr; specificClass) pattern |
| [Topology-Patterns](Topology-Patterns) | See typical plant configs end-to-end with verified edges. | | [Topology Patterns](Topology-Patterns) | See typical plant configs end-to-end with verified edges |
| Pick a node | The most mature is [pumpingStation](https://gitea.wbd-rd.nl/RnD/pumpingStation/wiki/Home) (refactor pilot). | | Pick a node | Most mature is [pumpingStation](https://gitea.wbd-rd.nl/RnD/pumpingStation/wiki/Home) (refactor pilot) |
| [Topic-Conventions](Topic-Conventions) | Reference for naming when you start wiring your own flows. | | [Topic Conventions](Topic-Conventions) | Reference for naming when you wire your own flows |
| [Telemetry](Telemetry) | If you're plumbing InfluxDB or Grafana. | | [Telemetry](Telemetry) | If you are plumbing InfluxDB or Grafana |
---
## Quick command reference ## Quick command reference
```bash ```bash
# run all tests # All tests
npm run test:platform npm run test:platform
# run one node's tests # One node's tests
cd nodes/rotatingMachine && node --test test/basic/*.test.js cd nodes/rotatingMachine && node --test test/basic/*.test.js
# regenerate a node's wiki AUTOGEN blocks # Regenerate a node's wiki AUTOGEN blocks
cd nodes/rotatingMachine && npm run wiki:all cd nodes/rotatingMachine && npm run wiki:all
# rebuild docker stack # Rebuild docker stack
docker compose build && docker compose up -d docker compose build && docker compose up -d
# update all submodules to their development tips # Fetch all submodules to their development tips
git submodule update --remote --recursive git submodule update --remote --recursive
# pack EVOLV as an npm tarball # Pack EVOLV as an npm tarball
npm pack npm pack
``` ```
---
## Where to ask for help ## Where to ask for help
| Channel | Use it for | | Channel | Use it for |
|---|---| |:---|:---|
| Per-node wiki on Gitea | Operator-level questions for one node. | | Per-node wiki on Gitea | Operator-level questions for one node |
| `.claude/refactor/OPEN_QUESTIONS.md` | Live decisions log issues being worked on. | | `.claude/refactor/OPEN_QUESTIONS.md` | Live decisions log &mdash; issues being worked on |
| Gitea repo issues per submodule | File a bug against a specific node. | | Gitea repo issues per submodule | File a bug against a specific node |
| R&D team Slack / Teams | Anything urgent or strategic. | | R&D team Slack / Teams | Anything urgent or strategic |
---
## Related pages ## Related pages
- [Home](Home) — top-level navigation | Page | Why |
- [Architecture](Architecture) — how a node is built |:---|:---|
- [Topology-Patterns](Topology-Patterns) — plant configurations | [Home](Home) | Top-level navigation |
| [Architecture](Architecture) | How a node is built |
| [Topology Patterns](Topology-Patterns) | Plant configurations |
| [Glossary](Glossary) | Decode S88 / EVOLV jargon |

@@ -1,23 +1,33 @@
# Glossary # Glossary
> **Reflects code as of `9ab9f6b` · regenerated `2026-05-11`** ![code-ref](https://img.shields.io/badge/code--ref-9ab9f6b-blue)
Terms and abbreviations used across the EVOLV codebase, wikis, and dashboards. > [!NOTE]
> S88, EVOLV runtime, wastewater, pump / hydraulics, control, and project terms. Use this page as a dictionary while reading the other wiki pages.
## S88 (ISA-88 batch control) ---
## S88 &mdash; ISA-88 batch control
```mermaid ```mermaid
flowchart TB flowchart TB
enterprise["Enterprise"]:::neutral enterprise["Enterprise"]
site["Site"]:::neutral site["Site"]
area["Area"]:::area area["Area"]
pc["Process Cell"]:::pc pc["Process Cell"]
unit["Unit"]:::unit unit["Unit"]
em["Equipment Module"]:::equip em["Equipment Module"]
cm["Control Module"]:::ctrl cm["Control Module"]
enterprise --> site --> area --> pc --> unit --> em --> cm enterprise --> site --> area --> pc --> unit --> em --> cm
class enterprise,site neutral
class area area
class pc pc
class unit unit
class em equip
class cm ctrl
classDef neutral fill:#dddddd classDef neutral fill:#dddddd
classDef area fill:#0f52a5,color:#fff classDef area fill:#0f52a5,color:#fff
classDef pc fill:#0c99d9,color:#fff classDef pc fill:#0c99d9,color:#fff
@@ -27,106 +37,131 @@ flowchart TB
``` ```
| Term | Meaning in EVOLV | | Term | Meaning in EVOLV |
|---|---| |:---|:---|
| **Area** | Plant section. *Reserved*, no node implements it yet. | | Area | Plant section. Reserved &mdash; no node implements it yet |
| **Process Cell (PC)** | Self-contained sub-process. `pumpingStation` is the only PC-level node. | | Process Cell (PC) | Self-contained sub-process. pumpingStation is the only PC-level node |
| **Unit (UN)** | One major piece of equipment or a coordinator over equipment. `MGC`, `VGC`, `reactor`, `settler`, `monster`. | | Unit (UN) | One major piece of equipment or a coordinator. MGC, VGC, reactor, settler, monster |
| **Equipment Module (EM)** | A single piece of equipment. `rotatingMachine`, `valve`, `diffuser`. | | Equipment Module (EM) | A single piece of equipment. rotatingMachine, valve, diffuser |
| **Control Module (CM)** | Single sensor / actuator. `measurement`. | | Control Module (CM) | A single sensor or actuator. measurement |
| **softwareType** | The node's S88 type — used by `ChildRouter` to match `child.register` calls. | | softwareType | The node's S88 type. Used by `ChildRouter` to match `child.register` |
## EVOLV runtime concepts ---
## EVOLV runtime
| Term | Meaning | | Term | Meaning |
|---|---| |:---|:---|
| **BaseDomain** | Base class for every specificClass. Owns `measurements`, `router`, `emitter`, `logger`. | | BaseDomain | Base class for every specificClass. Owns `measurements`, `router`, `emitter`, `logger` |
| **BaseNodeAdapter** | Base class for every nodeClass. Bridges Node-RED specificClass via `commandRegistry`. | | BaseNodeAdapter | Base class for every nodeClass. Bridges Node-RED to specificClass via `commandRegistry` |
| **specificClass** | Pure-JS domain logic. No `RED.*` imports. Unit-testable in isolation. | | specificClass | Pure-JS domain logic. No `RED.*` imports. Unit-testable in isolation |
| **nodeClass** | Node-RED adapter — owns input routing, tick loop, port wiring, status badge. | | nodeClass | Node-RED adapter &mdash; input routing, tick loop, port wiring, status badge |
| **ChildRouter** | Declarative matcher for `child.register` events. `router.onRegister(swType, handler)`. | | ChildRouter | Declarative matcher for `child.register`. `router.onRegister(swType, handler)` |
| **commandRegistry** | Inbound-msg dispatcher. Topic → handler descriptor map; resolves aliases and coerces units. | | commandRegistry | Inbound-msg dispatcher. Topic to descriptor; resolves aliases, coerces units |
| **UnitPolicy** | Per-node declaration of canonical (internal) and output units. | | UnitPolicy | Per-node declaration of canonical (internal) and output units |
| **MeasurementContainer** | Chainable measurement store. Keys: `<type>.<variant>.<position>.<childId>`. | | MeasurementContainer | Chainable measurement store. Keys: `<type>.<variant>.<position>.<childId>` |
| **statusBadge** | Composes `node.status({fill,shape,text})` from a HealthStatus. | | statusBadge | Composes `node.status({fill, shape, text})` from a HealthStatus |
| **HealthStatus** | `{level: 0..3, flags, message, source}` standardised health summary. | | HealthStatus | `{level: 0..3, flags, message, source}` &mdash; standardised health summary |
| **LatestWinsGate** | Mutex with supersede semantics — superseded calls resolve with `{superseded: true}`. | | LatestWinsGate | Mutex with supersede semantics. Superseded calls resolve with `{superseded: true}` |
| **outputUtils** | Single point for Port-0 delta compression + Port-1 line-protocol formatting. | | outputUtils | Port-0 delta compression and Port-1 line-protocol formatting |
| **tick** | The 1 Hz update loop. Domain runs `tick()` for time-based concerns (integrators, FSM timers). | | tick | 1 Hz update loop. Opt-in via `static tickInterval = N` on nodeClass |
| **getOutput()** | Domain method returning the current snapshot — fed to `outputUtils` for diff/format. | | `'output-changed'` | Event the specificClass fires on `this.emitter` when public state shifts |
| **getFlattenedOutput()** | Returns measurements with dot-flattened keys (4-segment: `type.variant.position.childId`). | | `getOutput()` | Domain method returning the current snapshot &mdash; fed to `outputUtils` |
| `getFlattenedOutput()` | Measurements with dot-flattened keys (4-segment `type.variant.position.childId`) |
---
## Topic prefixes ## Topic prefixes
(See [Topic-Conventions](Topic-Conventions) for the full table.) See [Topic Conventions](Topic-Conventions) for the full reference.
| Prefix | Direction | Idempotent? | | Prefix | Direction | Idempotent |
|---|---|---| |:---|:---|:---|
| `set.` | inbound | yes | | `set.` | in | Yes |
| `cmd.` | inbound | no — has side-effects | | `cmd.` | in | No (side-effects) |
| `data.` | bidirectional | n/a | | `data.` | in / out | n/a |
| `evt.` | outbound | n/a | | `query.` | in | Yes (read-only) |
| `child.` | inbound (parent receives) | yes id-keyed | | `child.` | in (parent) | Yes (id-keyed) |
| `evt.` | out | n/a |
---
## Wastewater treatment terms ## Wastewater treatment terms
| Term | Meaning | | Term | Meaning |
|---|---| |:---|:---|
| **WWTP** | Wastewater Treatment Plant. | | WWTP | Wastewater Treatment Plant |
| **Influent / Effluent** | Inlet / outlet stream of a process unit. | | Influent / Effluent | Inlet / outlet stream of a process unit |
| **Activated Sludge** | Biological process where bacteria consume organic matter under aeration. Modelled by ASM (ASM1, ASM3, …). | | Activated Sludge | Biological process &mdash; bacteria consume organic matter under aeration. Modelled by ASM1, ASM3 |
| **MLSS** | Mixed Liquor Suspended Solids biomass concentration in the reactor. | | MLSS | Mixed Liquor Suspended Solids &mdash; biomass concentration in the reactor |
| **RAS** | Return Activated Sludge settled sludge pumped back from settler to reactor. | | RAS | Return Activated Sludge &mdash; settled sludge pumped back from settler to reactor |
| **WAS** | Waste Activated Sludge excess sludge removed from the system. | | WAS | Waste Activated Sludge &mdash; excess sludge removed from the system |
| **TSS** | Total Suspended Solids. | | TSS | Total Suspended Solids |
| **COD / BOD** | Chemical / Biological Oxygen Demand organic load metrics. | | COD / BOD | Chemical / Biological Oxygen Demand &mdash; organic load metrics |
| **NH / NO** | Ammonium / Nitrate N species, key for nitrification/denitrification. | | NH4 / NO3 | Ammonium / Nitrate &mdash; N species, key for nitrification / denitrification |
| **DO** | Dissolved Oxygen, mg/L. Setpoint typically ~2 mg/L in aerated zones. | | DO | Dissolved Oxygen (mg/L). Setpoint typically around 2 mg/L in aerated zones |
| **K_La** | Volumetric mass-transfer coefficient (oxygen transfer rate / driving force). | | K_La | Volumetric mass-transfer coefficient (oxygen transfer rate / driving force) |
| **OTR** | Oxygen Transfer Rate diffuser's output to the reactor. | | OTR | Oxygen Transfer Rate &mdash; diffuser's output to the reactor |
| **HRT / SRT** | Hydraulic / Sludge Retention Time. | | HRT / SRT | Hydraulic / Sludge Retention Time |
| **F/M ratio** | Food-to-microorganism ratio. | | F/M ratio | Food-to-microorganism ratio |
| **Composite sample** | A sample built up over time, often proportional to flow — what `monster` simulates. | | Composite sample | A sample built over time, often flow-proportional &mdash; what monster simulates |
See [ASM Models](Concept-ASM-Models) for biological kinetics in detail.
---
## Pump / hydraulics terms ## Pump / hydraulics terms
| Term | Meaning | | Term | Meaning |
|---|---| |:---|:---|
| **BEP** | Best Efficiency Point operating point where the pump consumes the least energy per unit flow. | | BEP | Best Efficiency Point &mdash; operating point with minimum energy per unit flow |
| **NPSH** | Net Positive Suction Head pressure margin to avoid cavitation. | | NPSH | Net Positive Suction Head &mdash; pressure margin to avoid cavitation |
| **Affinity laws** | Q ∝ N, H ∝ N², P ∝ N³ — how flow/head/power scale with pump speed. | | Affinity laws | Q proportional N, H proportional N-squared, P proportional N-cubed &mdash; how flow / head / power scale with pump speed |
| **Characteristic curve** | Q H (or Q P, Q ↔ η) graph supplied by the pump manufacturer. | | Characteristic curve | Q to H (or Q to P, Q to efficiency) graph supplied by the pump manufacturer |
| **NCog** | Normalised cost-of-going metric used by MGC for switching stability. | | NCog | Normalised cost-of-going metric used by MGC for switching stability |
| **Wet-well basin** | The buffer volume on a lift station what `pumpingStation` models. | | Wet-well basin | Buffer volume on a lift station &mdash; what pumpingStation models |
| **Setpoint** | The commanded value (e.g., flow setpoint = 12 m³/h). | | Setpoint | Commanded value (e.g. flow setpoint = 12 m3/h) |
| **Demand** | The integrated requirement (e.g., the parent's "need this much flow now"). | | Demand | Integrated requirement (e.g. parent's "need this much flow now") |
See [Pump Affinity Laws](Concept-Pump-Affinity-Laws) and [BEP Gravitation Proof](Finding-BEP-Gravitation-Proof).
---
## Control / signal-processing terms ## Control / signal-processing terms
| Term | Meaning | | Term | Meaning |
|---|---| |:---|:---|
| **PID** | Proportional-Integral-Derivative controller. | | PID | Proportional-Integral-Derivative controller |
| **Anti-windup** | Prevents integral term from growing unboundedly when actuator saturates. | | Anti-windup | Prevents the integral term from growing unboundedly when the actuator saturates |
| **Hysteresis** | Switching threshold with a deadband (e.g., start at 80%, stop at 30%). | | Hysteresis | Switching threshold with a deadband (e.g. start at 80%, stop at 30%) |
| **Schmitt trigger** | Hysteresis-based binary switch (used for `stopLevel` in `pumpingStation`). | | Schmitt trigger | Hysteresis-based binary switch (used for `stopLevel` in pumpingStation) |
| **Smoothing** | Filtering noise (moving average, exponential, Savitzky-Golay). | | Smoothing | Filtering noise (moving average, exponential, Savitzky-Golay) |
| **Outlier detection** | Identifying readings that fall outside expected variance. | | Outlier detection | Identifying readings outside expected variance |
| **FSM** | Finite State Machine discrete states + transitions. | | FSM | Finite State Machine &mdash; discrete states + transitions |
See [PID Control Theory](Concept-PID-Control-Theory) and [Signal Processing &mdash; Sensors](Concept-Signal-Processing-Sensors).
---
## Project terms ## Project terms
| Term | Meaning | | Term | Meaning |
|---|---| |:---|:---|
| **Tier 14** | Refactor phases (see [Home](Home) project status). | | Tier 1&ndash;4 | Refactor phases. See [Home](Home) Refactor status |
| **Wave A / B / C** | Sub-batches of submodule pointer bumps during the refactor. | | Wave A / B / C | Sub-batches of submodule pointer bumps during the refactor |
| **OPEN_QUESTIONS.md** | Live decisions log at `.claude/refactor/OPEN_QUESTIONS.md`. | | OPEN_QUESTIONS.md | Live decisions log at `.claude/refactor/OPEN_QUESTIONS.md` |
| **CONTRACTS.md** | API shapes at `.claude/refactor/CONTRACTS.md`. | | CONTRACTS.md | API shapes at `.claude/refactor/CONTRACTS.md` |
| **MODULE_SPLIT.md** | Per-node concern layout at `.claude/refactor/MODULE_SPLIT.md`. | | MODULE_SPLIT.md | Per-node concern layout at `.claude/refactor/MODULE_SPLIT.md` |
| **WIKI_TEMPLATE.md** | 14-section per-node wiki template at `.claude/refactor/WIKI_TEMPLATE.md`. | | CONVENTIONS.md | Code style and naming at `.claude/refactor/CONVENTIONS.md` |
| **AUTOGEN block** | Sections of a wiki regenerated by `npm run wiki:all` — do not hand-edit between markers. | | WIKI_TEMPLATE.md | Per-node 14-section template at `.claude/refactor/WIKI_TEMPLATE.md` |
| AUTOGEN block | Sections regenerated by `npm run wiki:all`. Do not hand-edit between markers |
---
## Related pages ## Related pages
- [Home](Home) | Page | Why |
- [Architecture](Architecture) |:---|:---|
- [Topic-Conventions](Topic-Conventions) | [Home](Home) | Top-level node map |
- [Topology-Patterns](Topology-Patterns) | [Architecture](Architecture) | Runtime concepts in context |
| [Topic Conventions](Topic-Conventions) | Full topic / unit / colour reference |
| [Topology Patterns](Topology-Patterns) | Terms used in plant scenarios |

222
Home.md

@@ -1,34 +1,41 @@
# EVOLV — Wastewater Treatment Plant Automation # EVOLV — Wastewater Treatment Plant Automation
> **Reflects code as of `9ab9f6b` · regenerated `2026-05-11`** ![code-ref](https://img.shields.io/badge/code--ref-9ab9f6b-blue)
> Source of truth: `nodes/<name>/src/specificClass.js` `configure()` declarations. Edges below were verified against `router.onRegister(...)` calls and emitter subscriptions. ![platform-tests](https://img.shields.io/badge/platform_tests-823%2F823-brightgreen)
![branch](https://img.shields.io/badge/branch-development-orange)
![nodes](https://img.shields.io/badge/active_nodes-11_%2B_library-blue)
EVOLV is a Node-RED node library for wastewater plant automation, developed by Waterschap Brabantse Delta's R&D team. Nodes follow ISA-88 (S88). The library exposes **11 active nodes** across four S88 levels plus **1 utility node** for Grafana dashboard provisioning, all built on a shared `generalFunctions` library. > [!NOTE]
> EVOLV is a Node-RED node library for wastewater plant automation, built by Waterschap Brabantse Delta R&D. It exposes 11 active nodes across four ISA-88 (S88) levels plus one Grafana-provisioning utility, all on a shared `generalFunctions` library. Every node follows the same three-tier code shape: entry registers the node type; `nodeClass` (extends `BaseNodeAdapter`) bridges to the Node-RED runtime; `specificClass` (extends `BaseDomain`) holds pure-JS domain logic. Source of truth for everything claimed on this page: each node's `src/specificClass.js` `configure()` plus `.claude/refactor/CONTRACTS.md`.
---
## Platform overview ## Platform overview
Every solid arrow is a real `router.onRegister(<softwareType>, …)` call in the parent's `configure()`. Dashed arrows are emitter subscriptions (no child-register handshake). Thick `==>` arrows are Unit-to-Unit `stateChange` subscriptions. Verified node by node against current source.
```mermaid ```mermaid
flowchart TB flowchart TB
subgraph PC["Process Cell"] subgraph PC["Process Cell"]
ps[pumpingStation]:::pc ps[pumpingStation]:::pc
end end
subgraph UN["Unit"] subgraph UN["Unit"]
mgc[machineGroupControl]:::unit mgc[machineGroupControl]
vgc[valveGroupControl]:::unit vgc[valveGroupControl]
reactor[reactor]:::unit reactor[reactor]
settler[settler]:::unit settler[settler]
monster[monster]:::unit monster[monster]
end end
subgraph EM["Equipment"] subgraph EM["Equipment Module"]
rm[rotatingMachine]:::equip rm[rotatingMachine]
v[valve]:::equip v[valve]
diff[diffuser]:::equip diff[diffuser]
end end
subgraph CM["Control Module"] subgraph CM["Control Module"]
meas["measurement<br/><i>registers with any process node</i>"]:::ctrl meas["measurement &mdash; registers with any process node"]
end end
subgraph UT["Utility"] subgraph UT["Utility"]
dash["dashboardAPI<br/><i>any node Grafana dashboard</i>"]:::util dash["dashboardAPI &mdash; any node &rarr; Grafana"]
end end
ps -->|owns| mgc ps -->|owns| mgc
@@ -40,86 +47,149 @@ flowchart TB
reactor ==stateChange==> settler reactor ==stateChange==> settler
diff -. OTR data .-> reactor diff -. OTR data .-> reactor
classDef pc fill:#0c99d9,color:#fff class ps pc
classDef unit fill:#50a8d9,color:#000 class mgc,vgc,reactor,settler,monster unit
classDef equip fill:#86bbdd,color:#000 class rm,v,diff equip
classDef ctrl fill:#a9daee,color:#000 class meas ctrl
classDef util fill:#dddddd,color:#000 class dash util
classDef pc fill:#0c99d9,color:#fff,stroke:#075a82,stroke-width:2px
classDef unit fill:#50a8d9,color:#000,stroke:#2c7ba8,stroke-width:2px
classDef equip fill:#86bbdd,color:#000,stroke:#5a90b2,stroke-width:2px
classDef ctrl fill:#a9daee,color:#000,stroke:#76b7d4,stroke-width:2px
classDef util fill:#dddddd,color:#000,stroke:#a8a8a8,stroke-width:2px
``` ```
**Edges in this diagram are ground-truth** — every solid arrow is a `router.onRegister(softwareType, …)` declaration in the parent's `configure()`. Dashed arrows are emitter subscriptions (not child registrations). For full data-flow including `measurement` fan-out to every process node and `valveGroupControl`'s flow-source registrations, see **[Topology-Patterns](Topology-Patterns)**. ### Edge legend
| Arrow | Meaning | Implementation |
|---|---|---|
| `A --> B` (solid) | A owns B as an S88 child via `child.register` | `router.onRegister('<swType>', ...)` |
| `A -.-> B` (dashed) | A subscribes to B's emitter events; no handshake | `B.emitter.on('<event>', ...)` |
| `A ==> B` (thick) | Unit-to-Unit `stateChange` subscription | `settler._connectReactor` pattern |
### S88 colours
| Hex | Level | Used by |
|:---|:---|:---|
| `#0f52a5` | Area | Reserved &mdash; no node yet |
| `#0c99d9` | Process Cell | pumpingStation |
| `#50a8d9` | Unit | MGC, VGC, reactor, settler, monster |
| `#86bbdd` | Equipment Module | rotatingMachine, valve, diffuser |
| `#a9daee` | Control Module | measurement |
| `#dddddd` | Utility / neutral | dashboardAPI, helper functions |
Source: `.claude/rules/node-red-flow-layout.md` §14.
For the full data-flow map &mdash; every `measurement -> parent` edge, VGC's flow-source registrations, dashboardAPI's provisioning calls, and the worked plant example &mdash; see [Topology Patterns](Topology-Patterns).
---
## Live nodes ## Live nodes
| S88 level | Node | One-liner | Per-node wiki | | S88 level | Node | Role | Per-node wiki |
|---|---|---|---| |:---|:---|:---|:---|
| 🟦 Process Cell | **pumpingStation** | Wet-well basin model; dispatches demand to one or more pump groups. | [Home →](https://gitea.wbd-rd.nl/RnD/pumpingStation/wiki/Home) | | Process Cell | pumpingStation | Wet-well basin model; dispatches demand to pump groups. | [Open](https://gitea.wbd-rd.nl/RnD/pumpingStation/wiki/Home) |
| 🔷 Unit | **machineGroupControl** | Load-sharing across a group of `rotatingMachine` children. | [Home →](https://gitea.wbd-rd.nl/RnD/machineGroupControl/wiki/Home) | | Unit | machineGroupControl | Load-sharing across a group of `rotatingMachine` children. | [Open](https://gitea.wbd-rd.nl/RnD/machineGroupControl/wiki/Home) |
| 🔷 Unit | **valveGroupControl** | Coordinated position control across a group of `valve` children; can register pump/PS/MGC nodes as flow sources. | [Home →](https://gitea.wbd-rd.nl/RnD/valveGroupControl/wiki/Home) | | Unit | valveGroupControl | Coordinated position control over `valve` children. Registers upstream pumps / PS / MGC as flow sources. | [Open](https://gitea.wbd-rd.nl/RnD/valveGroupControl/wiki/Home) |
| 🔷 Unit | **reactor** | Bioreactor ASM kinetics (CSTR/PFR engines); pairs with diffuser + downstream settler. | [Home →](https://gitea.wbd-rd.nl/RnD/reactor/wiki/Home) | | Unit | reactor | Bioreactor &mdash; ASM kinetics (CSTR / PFR). Pairs with diffuser and downstream settler. | [Open](https://gitea.wbd-rd.nl/RnD/reactor/wiki/Home) |
| 🔷 Unit | **settler** | Secondary clarifier; subscribes to upstream reactor stateChange, drives a return-pump. | [Home →](https://gitea.wbd-rd.nl/RnD/settler/wiki/Home) | | Unit | settler | Secondary clarifier; subscribes to upstream reactor `stateChange`, drives a return pump. | [Open](https://gitea.wbd-rd.nl/RnD/settler/wiki/Home) |
| 🔷 Unit | **monster** | Composite-sample sensor surrogate / proportional sampling program. | [Home →](https://gitea.wbd-rd.nl/RnD/monster/wiki/Home) | | Unit | monster | Composite-sample sensor surrogate; proportional sampling program. | [Open](https://gitea.wbd-rd.nl/RnD/monster/wiki/Home) |
| 🟦 Equipment | **rotatingMachine** | Single pump / compressor characteristic curves, prediction, FSM. | [Home →](https://gitea.wbd-rd.nl/RnD/rotatingMachine/wiki/Home) | | Equipment | rotatingMachine | Single pump or compressor &mdash; characteristic curves, prediction, full FSM. | [Open](https://gitea.wbd-rd.nl/RnD/rotatingMachine/wiki/Home) |
| 🟦 Equipment | **valve** | Single valve actuator with FSM (shared with rotatingMachine state model). | [Home →](https://gitea.wbd-rd.nl/RnD/valve/wiki/Home) | | Equipment | valve | Single valve actuator &mdash; shared FSM with rotatingMachine. | [Open](https://gitea.wbd-rd.nl/RnD/valve/wiki/Home) |
| 🟦 Equipment | **diffuser** | Aeration diffuser; gas-side modelling, OTR emission to reactor. | [Home →](https://gitea.wbd-rd.nl/RnD/diffuser/wiki/Home) | | Equipment | diffuser | Aeration diffuser; gas-side modelling, OTR emission. | [Open](https://gitea.wbd-rd.nl/RnD/diffuser/wiki/Home) |
| 🔹 Control Module | **measurement** | Sensor signal-conditioning, scaling, smoothing, outlier detection, analog/digital/MQTT. | [Home →](https://gitea.wbd-rd.nl/RnD/measurement/wiki/Home) | | Control Module | measurement | Sensor signal conditioning &mdash; scaling, smoothing, outlier detection; analog / digital / MQTT modes. | [Open](https://gitea.wbd-rd.nl/RnD/measurement/wiki/Home) |
| Utility | **dashboardAPI** | Receives `child.register` for any process node provisions Grafana dashboard via HTTP. | [Home →](https://gitea.wbd-rd.nl/RnD/dashboardAPI/wiki/Home) | | Utility | dashboardAPI | Receives `child.register` from any node; provisions a Grafana dashboard via HTTP. | [Open](https://gitea.wbd-rd.nl/RnD/dashboardAPI/wiki/Home) |
| — | **generalFunctions** | Shared library — `BaseDomain`, `BaseNodeAdapter`, `ChildRouter`, `commandRegistry`, `UnitPolicy`, `MeasurementContainer`, `statusBadge`, `HealthStatus`, `logger`, `configManager`. **Not a Node-RED node.** | [Home →](https://gitea.wbd-rd.nl/RnD/generalFunctions/wiki/Home) | | Library | generalFunctions | `BaseDomain`, `BaseNodeAdapter`, `ChildRouter`, commandRegistry, `UnitPolicy`, `MeasurementContainer`, `statusBadge`, `HealthStatus`, `LatestWinsGate`, logger, configManager. Not a Node-RED node. | [Open](https://gitea.wbd-rd.nl/RnD/generalFunctions/wiki/Home) |
---
## Start here ## Start here
| You want to | Read | | If you want to&hellip; | Read |
|---|---| |:---|:---|
| Stand up a local dev environment + run an example flow | [Getting-Started](Getting-Started) | | Stand up a local dev environment and run an example flow | [Getting Started](Getting-Started) |
| Understand the codebase layout, BaseDomain/adapter pattern, output ports | [Architecture](Architecture) | | Understand the codebase layout, BaseDomain / adapter pattern, output ports | [Architecture](Architecture) |
| See typical plant configurations and how nodes wire together | [Topology-Patterns](Topology-Patterns) | | See typical plant configurations and how nodes wire together | [Topology Patterns](Topology-Patterns) |
| Know what topic names to use, units, S88 colours | [Topic-Conventions](Topic-Conventions) | | Know what topic names to use, units, S88 colours, measurement keys | [Topic Conventions](Topic-Conventions) |
| Understand what Port 0 / Port 1 / Port 2 carry, InfluxDB layout | [Telemetry](Telemetry) | | Understand what Port 0 / Port 1 / Port 2 carry, InfluxDB layout, Grafana | [Telemetry](Telemetry) |
| Decode S88 / EVOLV jargon | [Glossary](Glossary) | | Decode S88 / EVOLV / WWTP jargon | [Glossary](Glossary) |
## Domain concepts ---
Evergreen technical references (not affected by refactors): ## Domain concepts (evergreen)
Domain knowledge that doesn't change when code is refactored.
| Page | Topic | | Page | Topic |
|---|---| |:---|:---|
| [ASM models](concepts/asm-models) | Activated Sludge Models biological process kinetics | | [ASM Models](Concept-ASM-Models) | Activated Sludge Models &mdash; biological process kinetics |
| [PID control theory](concepts/pid-control-theory) | Loop tuning, anti-windup, controller forms | | [PID Control Theory](Concept-PID-Control-Theory) | Loop tuning, anti-windup, controller forms |
| [Pump affinity laws](concepts/pump-affinity-laws) | Speed/flow/head/power scaling | | [Pump Affinity Laws](Concept-Pump-Affinity-Laws) | Speed / flow / head / power scaling |
| [Settling models](concepts/settling-models) | Takács / Vesilind / discrete settling | | [Settling Models](Concept-Settling-Models) | Tak&aacute;cs / Vesilind / discrete settling |
| [Signal processing — sensors](concepts/signal-processing-sensors) | Smoothing, outlier rejection | | [Signal Processing &mdash; Sensors](Concept-Signal-Processing-Sensors) | Smoothing, outlier rejection |
| [InfluxDB schema design](concepts/influxdb-schema-design) | Cardinality, tags vs fields | | [InfluxDB Schema Design](Concept-InfluxDB-Schema-Design) | Cardinality, tags vs fields |
| [Wastewater compliance NL](concepts/wastewater-compliance-nl) | Dutch regulatory context | | [Wastewater Compliance NL](Concept-Wastewater-Compliance-NL) | Dutch regulatory context |
| [OT security IEC 62443](concepts/ot-security-iec62443) | OT cybersecurity baseline | | [OT Security &mdash; IEC 62443](Concept-OT-Security-IEC62443) | OT cybersecurity baseline |
## Operations findings ## Operations findings (algorithm proofs)
Algorithm-level proofs and behavioural notes that are still valid:
| Page | Topic | | Page | Topic |
|---|---| |:---|:---|
| [BEP gravitation proof](findings/bep-gravitation-proof) | Best-efficiency-point convergence | | [BEP Gravitation Proof](Finding-BEP-Gravitation-Proof) | Best-Efficiency-Point convergence |
| [Curve non-convexity](findings/curve-non-convexity) | When pump curves break local optima | | [Curve Non-Convexity](Finding-Curve-Non-Convexity) | When pump curves break local optima |
| [NCog behaviour](findings/ncog-behavior) | NCog control metric notes | | [NCog Behaviour](Finding-NCog-Behavior) | NCog control metric notes |
| [Pump switching stability](findings/pump-switching-stability) | Hysteresis design for multi-pump groups | | [Pump Switching Stability](Finding-Pump-Switching-Stability) | Hysteresis design for multi-pump groups |
## Project status ## Node-RED / FlowFuse manuals
| Tier | What | Status | | Page | Topic |
|---|---|---| |:---|:---|
| 1 | Add infra in `generalFunctions` (additive only) | ✅ done | | [Manual Index](Manual-NodeRED-INDEX) | Top of the Node-RED reference set |
| 2 | Pilot: pumpingStation end-to-end on new infra | ✅ done | | [Runtime &mdash; Node.js](Manual-NodeRED-Runtime-Node-Js) | `send`, `done`, multi-output arrays |
| 3 | Convert measurement, MGC, rotatingMachine | ✅ done | | [Function Node Patterns](Manual-NodeRED-Function-Node-Patterns) | Return / send patterns |
| 4 | Convert valve, VGC, reactor, settler, monster, diffuser, dashboardAPI | ✅ done | | [Messages and Editor Structure](Manual-NodeRED-Messages-And-Editor-Structure) | Msg shape + HTML / editor / runtime split |
| 5 | Canonical topic names + alias deprecation map | ✅ done | | [FlowFuse ui-chart](Manual-NodeRED-Flowfuse-Ui-Chart-Manual) | Data contract, runtime controls |
| 6 | Promote `development``main` | ⏳ pending Docker E2E + human review | | [FlowFuse ui-button](Manual-NodeRED-Flowfuse-Ui-Button-Manual) | Button reference |
| 8.5 | Remove deprecated paths in `generalFunctions` | ✅ done | | [FlowFuse ui-gauge](Manual-NodeRED-Flowfuse-Ui-Gauge-Manual) | Gauge reference |
| 9 | Wiki refactor — visual-first per-node + master pages | ✅ landed 2026-05-11 | | [FlowFuse ui-text](Manual-NodeRED-Flowfuse-Ui-Text-Manual) | Text reference |
| 10 | Test-suite refactor across all nodes | 🟡 in progress | | [FlowFuse ui-template](Manual-NodeRED-Flowfuse-Ui-Template-Manual) | Template reference |
| — | pumpingStation Docker E2E (P2.14) | ⏳ pending | | [FlowFuse ui-config](Manual-NodeRED-Flowfuse-Ui-Config-Manual) | Config reference |
| [Dashboard Layout](Manual-NodeRED-Flowfuse-Dashboard-Layout-Manual) | Compact layout guidance |
| [Widgets Catalog](Manual-NodeRED-Flowfuse-Widgets-Catalog) | All widgets at a glance |
823 platform tests pass · 0 failures · 12 submodules + parent on `development`. ---
## Refactor status
| Tier | Scope | Status |
|:---:|:---|:---|
| 1 | Add infra in `generalFunctions` (additive only) | Done |
| 2 | Pilot: pumpingStation on new infra | Done |
| 3 | Convert measurement, MGC, rotatingMachine | Done |
| 4 | Convert valve, VGC, reactor, settler, monster, diffuser, dashboardAPI | Done |
| 5 | Canonical topic names + alias deprecation map | Done |
| 6 | Promote `development` &rarr; `main` | Pending Docker E2E + human review |
| 8.5 | Remove deprecated paths in `generalFunctions` | Done |
| 9 | Visual-first wiki refactor &mdash; per-node + master | Done 2026-05-11 |
| 10 | Test-suite refactor across all nodes | In progress |
> [!IMPORTANT]
> Active branch is `development` everywhere &mdash; 12 submodules plus parent EVOLV. `main` will receive the merged refactor only after Docker E2E sign-off (pumpingStation P2.14 pending).
---
## Archive ## Archive
Pre-refactor planning pages have been moved to the [Archive](Archive). The current Home and supporting pages are the canonical references. Pre-refactor wiki content has been removed from the live wiki. The git history of `EVOLV.wiki.git` preserves every prior page if you need to consult it. See [Archive](Archive) for the changelog of what was removed and when.
---
## Need help?
| Channel | Use it for |
|:---|:---|
| Per-node wiki on Gitea | Operator-level questions for one node |
| `.claude/refactor/OPEN_QUESTIONS.md` | Live decisions log &mdash; issues being worked on |
| Submodule issues on Gitea | File a bug against a specific node |
| R&D team Slack / Teams | Anything urgent or strategic |

@@ -1,5 +1,11 @@
# FlowFuse Dashboard Layout Notes (EVOLV Reference) # FlowFuse Dashboard Layout Notes (EVOLV Reference)
![code-ref](https://img.shields.io/badge/code--ref-9ab9f6b-blue) ![type](https://img.shields.io/badge/type-manual_(third-party)-orange)
> [!NOTE]
> Reference page. Maintained for context; not regenerated by code. See [Home](Home) for current top-level navigation.
Primary sources: Primary sources:
- https://dashboard.flowfuse.com/ - https://dashboard.flowfuse.com/
- https://dashboard.flowfuse.com/nodes/widgets/ui-chart.html - https://dashboard.flowfuse.com/nodes/widgets/ui-chart.html

@@ -1,4 +1,10 @@
# FlowFuse `ui-button` Manual (EVOLV Reference) # FlowFuse `ui-button` Manual (EVOLV Reference)
![code-ref](https://img.shields.io/badge/code--ref-9ab9f6b-blue) ![type](https://img.shields.io/badge/type-manual_(third-party)-orange)
> [!NOTE]
> Reference page. Maintained for context; not regenerated by code. See [Home](Home) for current top-level navigation.
Source: https://dashboard.flowfuse.com/nodes/widgets/ui-button.html Source: https://dashboard.flowfuse.com/nodes/widgets/ui-button.html

@@ -1,5 +1,11 @@
# FlowFuse `ui-chart` Manual (EVOLV Reference) # FlowFuse `ui-chart` Manual (EVOLV Reference)
![code-ref](https://img.shields.io/badge/code--ref-9ab9f6b-blue) ![type](https://img.shields.io/badge/type-manual_(third-party)-orange)
> [!NOTE]
> Reference page. Maintained for context; not regenerated by code. See [Home](Home) for current top-level navigation.
Source: https://dashboard.flowfuse.com/nodes/widgets/ui-chart.html Source: https://dashboard.flowfuse.com/nodes/widgets/ui-chart.html
## Chart Types ## Chart Types

@@ -1,4 +1,10 @@
# FlowFuse Config Nodes Manual (EVOLV Reference) # FlowFuse Config Nodes Manual (EVOLV Reference)
![code-ref](https://img.shields.io/badge/code--ref-9ab9f6b-blue) ![type](https://img.shields.io/badge/type-manual_(third-party)-orange)
> [!NOTE]
> Reference page. Maintained for context; not regenerated by code. See [Home](Home) for current top-level navigation.
Sources: Sources:
- https://dashboard.flowfuse.com/nodes/config/ui-base.html - https://dashboard.flowfuse.com/nodes/config/ui-base.html

@@ -1,4 +1,10 @@
# FlowFuse `ui-gauge` Manual (EVOLV Reference) # FlowFuse `ui-gauge` Manual (EVOLV Reference)
![code-ref](https://img.shields.io/badge/code--ref-9ab9f6b-blue) ![type](https://img.shields.io/badge/type-manual_(third-party)-orange)
> [!NOTE]
> Reference page. Maintained for context; not regenerated by code. See [Home](Home) for current top-level navigation.
Source: https://dashboard.flowfuse.com/nodes/widgets/ui-gauge.html Source: https://dashboard.flowfuse.com/nodes/widgets/ui-gauge.html

@@ -1,4 +1,10 @@
# FlowFuse `ui-template` Manual (EVOLV Reference) # FlowFuse `ui-template` Manual (EVOLV Reference)
![code-ref](https://img.shields.io/badge/code--ref-9ab9f6b-blue) ![type](https://img.shields.io/badge/type-manual_(third-party)-orange)
> [!NOTE]
> Reference page. Maintained for context; not regenerated by code. See [Home](Home) for current top-level navigation.
Source: https://dashboard.flowfuse.com/nodes/widgets/ui-template.html Source: https://dashboard.flowfuse.com/nodes/widgets/ui-template.html

@@ -1,4 +1,10 @@
# FlowFuse `ui-text` Manual (EVOLV Reference) # FlowFuse `ui-text` Manual (EVOLV Reference)
![code-ref](https://img.shields.io/badge/code--ref-9ab9f6b-blue) ![type](https://img.shields.io/badge/type-manual_(third-party)-orange)
> [!NOTE]
> Reference page. Maintained for context; not regenerated by code. See [Home](Home) for current top-level navigation.
Source: https://dashboard.flowfuse.com/nodes/widgets/ui-text.html Source: https://dashboard.flowfuse.com/nodes/widgets/ui-text.html

@@ -1,4 +1,10 @@
# FlowFuse Dashboard 2.0 — Widget Catalog # FlowFuse Dashboard 2.0 — Widget Catalog
![code-ref](https://img.shields.io/badge/code--ref-9ab9f6b-blue) ![type](https://img.shields.io/badge/type-manual_(third-party)-orange)
> [!NOTE]
> Reference page. Maintained for context; not regenerated by code. See [Home](Home) for current top-level navigation.
Source: https://dashboard.flowfuse.com/ Source: https://dashboard.flowfuse.com/

@@ -1,5 +1,11 @@
# Node-RED Function Node Patterns (EVOLV Summary) # Node-RED Function Node Patterns (EVOLV Summary)
![code-ref](https://img.shields.io/badge/code--ref-9ab9f6b-blue) ![type](https://img.shields.io/badge/type-manual_(third-party)-orange)
> [!NOTE]
> Reference page. Maintained for context; not regenerated by code. See [Home](Home) for current top-level navigation.
Based on: https://nodered.org/docs/user-guide/writing-functions Based on: https://nodered.org/docs/user-guide/writing-functions
## Return Semantics ## Return Semantics

@@ -1,5 +1,11 @@
# Node-RED Manual Index # Node-RED Manual Index
![code-ref](https://img.shields.io/badge/code--ref-9ab9f6b-blue) ![type](https://img.shields.io/badge/type-manual_(third-party)-orange)
> [!NOTE]
> Reference page. Maintained for context; not regenerated by code. See [Home](Home) for current top-level navigation.
This folder summarizes official Node-RED docs that are relevant to EVOLV node development. This folder summarizes official Node-RED docs that are relevant to EVOLV node development.
## Official Sources ## Official Sources

@@ -1,5 +1,11 @@
# Node-RED Messages and Editor/Runtime Structure (EVOLV Summary) # Node-RED Messages and Editor/Runtime Structure (EVOLV Summary)
![code-ref](https://img.shields.io/badge/code--ref-9ab9f6b-blue) ![type](https://img.shields.io/badge/type-manual_(third-party)-orange)
> [!NOTE]
> Reference page. Maintained for context; not regenerated by code. See [Home](Home) for current top-level navigation.
Sources: Sources:
- Messages: https://nodered.org/docs/user-guide/messages - Messages: https://nodered.org/docs/user-guide/messages
- Edit dialog and node definition: https://nodered.org/docs/creating-nodes/edit-dialog - Edit dialog and node definition: https://nodered.org/docs/creating-nodes/edit-dialog

@@ -1,5 +1,11 @@
# Node-RED Runtime Node JS Manual (EVOLV Summary) # Node-RED Runtime Node JS Manual (EVOLV Summary)
![code-ref](https://img.shields.io/badge/code--ref-9ab9f6b-blue) ![type](https://img.shields.io/badge/type-manual_(third-party)-orange)
> [!NOTE]
> Reference page. Maintained for context; not regenerated by code. See [Home](Home) for current top-level navigation.
Based on: https://nodered.org/docs/creating-nodes/node-js Based on: https://nodered.org/docs/creating-nodes/node-js
## Input Handler Contract ## Input Handler Contract

@@ -1,46 +1,68 @@
# Telemetry # Telemetry
> **Reflects code as of `9ab9f6b` · regenerated `2026-05-11`** ![code-ref](https://img.shields.io/badge/code--ref-9ab9f6b-blue)
![source](https://img.shields.io/badge/source-CONTRACTS.md_%C2%A710-orange)
What every EVOLV node emits on each of its three output ports, the InfluxDB line-protocol layout, and how the data reaches Grafana/FlowFuse. > [!NOTE]
> Every node sends on three output ports: Port 0 (process data), Port 1 (InfluxDB line protocol), Port 2 (registration / control plumbing). All output is formatted by `outputUtils.formatMsg` with delta compression: only changed fields are sent each tick. InfluxDB cardinality discipline: tags = identity (low cardinality), fields = numbers. Source of truth: `.claude/refactor/CONTRACTS.md` §10.
---
## Three-port model ## Three-port model
```mermaid ```mermaid
flowchart LR flowchart LR
sc[specificClass<br/>tick or event]:::tier3 sc["specificClass &mdash; 'output-changed' or tick()"]
nc[nodeClass<br/>outputUtils.formatMsg]:::tier2 ou["outputUtils.formatMsg &mdash; delta-compress"]
p0[(Port 0<br/>process)]:::p0 p0[("Port 0 &mdash; process")]
p1[(Port 1<br/>InfluxDB line)]:::p1 p1[("Port 1 &mdash; InfluxDB line")]
p2[(Port 2<br/>registration)]:::p2 p2[("Port 2 &mdash; register / control")]
dl["Downstream Node-RED &mdash; dashboards, functions"]
influx[("InfluxDB")]
parent["Parent EVOLV node"]
sc -->|getOutput| nc sc -- getOutput() --> ou
nc --> p0 ou --> p0 --> dl
nc --> p1 ou --> p1 --> influx
nc --> p2 sc -. child.register .-> p2 --> parent
p0 -. delta-compressed payload .-> dl[Downstream<br/>Node-RED logic]:::neutral class sc tier3
p1 -. line protocol .-> influx[(InfluxDB)]:::ext class ou tier2
p2 -. child.register .-> parent[Parent EVOLV node]:::neutral class p0 p0c
class p1 p1c
class p2 p2c
class dl,parent dn
class influx ext
classDef tier3 fill:#50a8d9,color:#000 classDef tier3 fill:#50a8d9,color:#000
classDef tier2 fill:#86bbdd,color:#000 classDef tier2 fill:#86bbdd,color:#000
classDef p0 fill:#86bbdd classDef p0c fill:#0c99d9,color:#fff
classDef p1 fill:#a9daee classDef p1c fill:#50a8d9,color:#000
classDef p2 fill:#dddddd classDef p2c fill:#a9daee,color:#000
classDef neutral fill:#dddddd classDef dn fill:#dddddd,color:#000
classDef ext fill:#fff2cc classDef ext fill:#fff2cc,color:#000
``` ```
## Port 0 — Process data (delta-compressed) ### Port summary
**Purpose:** feeds downstream Node-RED logic — dashboards, control functions, alarms. | Port | Carries | Format | Configured by | Trigger |
|:---|:---|:---|:---|:---|
| 0 (process) | Snapshot of changed measurement / state keys | JSON delta object | `outputUtils.formatMsg(..., 'process')` via `config.output.process` | `'output-changed'` on the emitter, or each tick |
| 1 (telemetry) | Numeric fields only &mdash; time-series | InfluxDB line protocol | `outputUtils.formatMsg(..., 'influxdb')` via `config.output.dbase` | Same trigger as Port 0 |
| 2 (register) | `child.register` plus internal control msgs | Plain object | `BaseNodeAdapter` init | Once 100ms after init; on demand |
**Shape:** `msg.payload` is an object containing **only keys that changed** since the last tick. Consumers cache + merge. ---
**Why delta-compressed:** at 1 Hz ticks with 50 fields per node, full snapshots flood downstream nodes and dashboards. Delta payloads typically carry 05 fields per tick. ## Port 0 &mdash; Process data (delta-compressed)
**Example (rotatingMachine, 1 tick):** Purpose: feed downstream Node-RED logic such as dashboards, alarms, control function nodes.
Shape: `msg.payload` contains only keys that changed since the last tick. Consumers must cache and merge.
> [!IMPORTANT]
> Why delta compression: at 1 Hz with 50+ fields per node, full snapshots flood downstream nodes and dashboards. A typical delta carries 0&ndash;5 fields per tick.
Example output, one rotatingMachine tick:
```json ```json
{ {
@@ -52,151 +74,192 @@ flowchart LR
} }
``` ```
Most other fields (state, pressure, mode, ) didn't change this tick omitted. Most other fields (state, pressure, mode, ...) didn't change this tick and are omitted.
**Trigger:** `outputUtils.checkForChanges()` compares the current `getOutput()` against the previous snapshot. Trigger: `outputUtils.checkForChanges()` compares the current `getOutput()` against the previous snapshot. No change means no send.
## Port 1 — Telemetry (InfluxDB line protocol) ---
**Purpose:** time-series storage in InfluxDB for trending, regulatory reporting, ML training. ## Port 1 &mdash; InfluxDB line protocol
**Shape:** `msg.payload` is a **string** (or array of strings) in InfluxDB line protocol: Purpose: time-series storage for trending, regulatory reporting, ML training.
Shape: `msg.payload` is a string (or array of strings) in InfluxDB line protocol:
``` ```
<measurement>,<tag-set> <field-set> <timestamp-ns> <measurement>,<tag-set> <field-set> <timestamp-ns>
``` ```
**Example:** Example, rotatingMachine telemetry:
``` ```
rotatingMachine,id=pump-A,softwareType=rotatingMachine flow_predicted_downstream=12.4,power_measured_atequipment=18.2 1714752000000000000 rotatingMachine,id=pump-A,softwareType=rotatingMachine flow_predicted_downstream=12.4,power_measured_atequipment=18.2 1714752000000000000
``` ```
**Conventions:** ### Conventions
| Element | Rule | | Element | Rule |
|---|---| |:---|:---|
| measurement (table) | The node's `softwareType` (lowercase). | | Measurement (table name) | The node's `softwareType`, lowercase |
| tag-set | Low-cardinality identity: `id`, `softwareType`, location-style tags. **Never** raw measurement values. | | Tag set | Low-cardinality identity: `id`, `softwareType`, location-style tags. Never raw measurement values |
| field-set | Numeric values only. Keys flatten `<type>_<variant>_<position>` (underscore, not dot InfluxDB constraint). | | Field set | Numeric values only. Keys flatten `<type>_<variant>_<position>` (underscore, not dot &mdash; InfluxDB constraint) |
| timestamp | Nanoseconds. Set by `outputUtils` from the node's clock. | | Timestamp | Nanoseconds. Set by `outputUtils` from the node's clock |
See [InfluxDB schema design](concepts/influxdb-schema-design) for the cardinality discipline. See [InfluxDB Schema Design](Concept-InfluxDB-Schema-Design) for cardinality discipline.
## Port 2 — Registration / control ---
**Purpose:** upward `child.register` at startup; later, internal control msgs (the registry-driven command replies). ## Port 2 &mdash; Registration / control
**Shape:** Purpose: upward `child.register` at startup; later, internal control msgs.
Shape:
```json ```json
{ {
"topic": "child.register", "topic": "child.register",
"payload": { "payload": {
"ref": <node reference>, "ref": "<node reference>",
"softwareType": "machine", "softwareType": "machine",
"config": { ... } "config": { }
} }
} }
``` ```
**Trigger:** the nodeClass adapter emits `child.register` on `init` if a `parent` is configured. The parent's `commandRegistry` dispatches into `ChildRouter.onRegister(...)`. Trigger: the nodeClass adapter emits `child.register` on init if a parent is configured. Parent's `commandRegistry` dispatches into `ChildRouter.onRegister(...)`.
The legacy alias `registerChild` is still accepted; it logs a one-time deprecation warning on first use.
---
## The output composition pipeline ## The output composition pipeline
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
participant tick as Tick (1 Hz) autonumber
participant tick as Tick (1 Hz) or event source
participant sc as specificClass participant sc as specificClass
participant mc as MeasurementContainer participant mc as MeasurementContainer
participant ou as outputUtils participant ou as outputUtils
participant ports as Ports 0 / 1 participant ports as Ports 0 / 1
tick->>sc: tick() tick->>sc: tick() OR emit('output-changed')
sc->>sc: concern modules update mc + state sc->>sc: concern modules update mc + state
sc->>ou: getOutput() snapshot sc->>ou: getOutput() snapshot
ou->>ou: diff vs last ou->>ou: diff vs last snapshot
alt no change alt no change
ou-->>sc: skip ou-->>sc: skip
else change else change
ou->>ports: Port 0 JSON delta ou->>ports: Port 0 &mdash; JSON delta
ou->>ports: Port 1 line protocol ou->>ports: Port 1 &mdash; line protocol
end end
``` ```
`outputUtils` is the single place the platform serialises state. Never write directly to `node.send` from specificClass — go through `outputUtils.formatMsg`. > [!CAUTION]
> Never write directly to `node.send` from specificClass. Go through `outputUtils.formatMsg`. Direct sends bypass delta compression and flood downstream nodes.
## InfluxDB layout ---
Recommended schema for EVOLV's data: ## InfluxDB schema layout (recommended)
| InfluxDB element | Maps to | | InfluxDB element | Maps to |
|---|---| |:---|:---|
| Database / bucket | One per plant (or per environment: `evolv_dev`, `evolv_prod`). | | Database / bucket | One per plant, or per environment: `evolv_dev`, `evolv_prod` |
| Measurement (table) | Node softwareType (`rotatingMachine`, `pumpingStation`, …). | | Measurement (table) | Node `softwareType` |
| Tags | `id` (instance id), `softwareType`, `area`, `processCell`, `unit` (for hierarchical drill-down). | | Tags | `id`, `softwareType`, `area`, `processCell`, `unit` (for hierarchical drill-down) |
| Fields | Numeric series — every key from `getOutput()` that has a numeric value, flattened with `_`. | | Fields | Numeric series &mdash; every numeric key from `getOutput()`, flattened with `_` |
| Retention | Hot bucket: 7 days @ 1 s. Cold bucket: 1 year @ 1 min downsample. | | Retention &mdash; hot | 7 days at 1 s |
| Retention &mdash; cold | 1 year at 1 min downsample |
**Cardinality discipline:** keep tag sets stable. Don't put `state` (string) as a tag — emit it as a field with code (`state_code=2`). High-cardinality tags fragment the index. > [!WARNING]
> Cardinality discipline. Keep tag sets stable. Do not put `state` (string) as a tag &mdash; emit it as a field with a code (`state_code=2`). High-cardinality tags fragment InfluxDB's index and degrade query performance dramatically.
See [InfluxDB Schema Design](Concept-InfluxDB-Schema-Design) for full guidance.
---
## FlowFuse dashboard wiring ## FlowFuse dashboard wiring
If you use FlowFuse `ui-chart` widgets, Port 0 is the natural source — the delta-compressed JSON maps cleanly to `msg.topic` (series label) + `msg.payload` (y-value). Port 0 is the natural source for FlowFuse `ui-chart` widgets &mdash; the delta-compressed JSON maps cleanly to `msg.topic` (series label) plus `msg.payload` (y-value).
Layout rule (from `.claude/rules/node-red-flow-layout.md` §4):
Layout rule for charts (from `.claude/rules/node-red-flow-layout.md` §4):
- One chart per metric type (one for flow, one for power, one for level). - One chart per metric type (one for flow, one for power, one for level).
- A trend-feeder function splits Port-0 deltas into per-series outputs, each output wired to one chart. - A trend-feeder function splits Port-0 deltas into per-series outputs.
- The chart's `category: "topic"` + `categoryType: "msg"` plots one series per unique `msg.topic`. - Each output wires to one chart. The chart's `category: "topic"` and `categoryType: "msg"` plot one series per unique `msg.topic`.
```mermaid ```mermaid
flowchart LR flowchart LR
p0[(Port 0)]:::p0 p0[("Port 0")]
split[trend-feeder<br/>function (N outputs)]:::tier2 split["trend-feeder &mdash; function (N outputs)"]
chart1[ui-chart: flow]:::neutral chart1["ui-chart: flow"]
chart2[ui-chart: power]:::neutral chart2["ui-chart: power"]
p0 --> split p0 --> split
split --> chart1 split --> chart1
split --> chart2 split --> chart2
classDef p0 fill:#86bbdd class p0 p0c
class split tier2
class chart1,chart2 neutral
classDef p0c fill:#0c99d9,color:#fff
classDef tier2 fill:#86bbdd,color:#000 classDef tier2 fill:#86bbdd,color:#000
classDef neutral fill:#dddddd classDef neutral fill:#dddddd
``` ```
See [FlowFuse ui-chart manual](Manual-NodeRED-Flowfuse-Ui-Chart-Manual) for the required chart properties.
---
## Grafana dashboard provisioning ## Grafana dashboard provisioning
`dashboardAPI` consumes registrations and emits Grafana dashboard JSON via HTTP. Wiring: `dashboardAPI` consumes registrations and emits Grafana dashboard JSON via HTTP.
```mermaid ```mermaid
flowchart LR flowchart LR
evolv[EVOLV node<br/>any softwareType]:::tier3 evolv["EVOLV node (any softwareType)"]
dash[dashboardAPI]:::util dash[dashboardAPI]
grafana[(Grafana HTTP API<br/>POST /api/dashboards/db)]:::ext grafana[("Grafana HTTP API &mdash; POST /api/dashboards/db")]
evolv -->|child.register| dash evolv -- child.register --> dash
dash -->|composed JSON| grafana dash -- composed JSON --> grafana
class evolv tier3
class dash util
class grafana ext
classDef tier3 fill:#50a8d9,color:#000 classDef tier3 fill:#50a8d9,color:#000
classDef util fill:#dddddd classDef util fill:#dddddd
classDef ext fill:#fff2cc classDef ext fill:#fff2cc,color:#000
``` ```
dashboardAPI looks up a template per softwareType (in `nodes/dashboardAPI/src/config/templates/`), substitutes the node's id + tags, and POSTs an upsert. Bearer-token auth is supported. dashboardAPI looks up a template per softwareType (in `nodes/dashboardAPI/src/config/templates/`), substitutes the node's id + tags, and POSTs an upsert. Bearer-token auth is supported via `config.grafanaConnector.bearerToken`.
## Common debug recipes ---
| Symptom | First check | ## Debug recipes
|---|---|
| InfluxDB rows missing for a node | Confirm Port 1 is wired to an `influxdb out` node. Tap Port 1 with a debug node to verify line-protocol output. | | Symptom | First thing to check |
| Dashboard widgets stuck on `n/a` | Confirm Port 0 is reaching the trend-feeder. Many widgets need `msg.topic` set for series labelling. | |:---|:---|
| `child.register` not arriving at parent | Tap Port 2 with debug. Confirm parent's `commandRegistry` accepts `child.register` (or the legacy `registerChild` alias). | | InfluxDB rows missing for a node | Port 1 wired to an `influxdb out` node? Tap Port 1 with a debug node to verify line-protocol output |
| Too many InfluxDB writes (high write-rate) | Check that `outputUtils.checkForChanges()` is firing. Likely you wired a tick-driven debug node bypassing the delta filter. | | Dashboard widgets stuck on `n/a` | Port 0 reaching the trend-feeder? Many widgets need `msg.topic` set for series labelling |
| Grafana dashboard not created on plant boot | Inspect dashboardAPI's HTTP response. Check the bearer token + base URL in its config. | | `child.register` not arriving | Tap Port 2 with debug. Confirm parent's `commandRegistry` accepts `child.register` (or `registerChild` alias) |
| Too many InfluxDB writes | Likely a tick-driven debug node bypassed the delta filter. Confirm `outputUtils.checkForChanges()` is firing |
| Grafana dashboard not created on boot | Inspect dashboardAPI's HTTP response. Check bearer token + base URL in its config |
| High cardinality alarm in InfluxDB | A string value is being written as a tag (probably `state` or similar). Move it to a field |
> [!CAUTION]
> Never ship `enableLog: 'debug'` in a demo. Fills the container log within seconds and obscures real errors. Use only for live debugging.
---
## Related pages ## Related pages
- [Architecture](Architecture) — output port wiring in the 3-tier code | Page | Why |
- [Topic-Conventions](Topic-Conventions) — what topics map to what fields |:---|:---|
- [InfluxDB schema design](concepts/influxdb-schema-design) — cardinality discipline | [Architecture](Architecture) | Output port wiring in the three-tier model |
| [Topic Conventions](Topic-Conventions) | What topics map to what fields |
| [Topology Patterns](Topology-Patterns) | Typical telemetry flows |
| [InfluxDB Schema Design](Concept-InfluxDB-Schema-Design) | Cardinality discipline |
| [FlowFuse ui-chart manual](Manual-NodeRED-Flowfuse-Ui-Chart-Manual) | Required chart properties |

@@ -1,80 +1,265 @@
# Topic Conventions # Topic Conventions
> **Reflects code as of `9ab9f6b` · regenerated `2026-05-11`** ![code-ref](https://img.shields.io/badge/code--ref-9ab9f6b-blue)
![source](https://img.shields.io/badge/source-CONTRACTS.md_%C2%A71-orange)
Naming rules, unit policy, and S88 colour palette. Source of truth: `.claude/refactor/CONTRACTS.md` §1. > [!NOTE]
> Every `msg.topic` in EVOLV uses one of six prefixes. The prefix tells you the kind (setter vs trigger vs data vs query vs lifecycle vs event), not the target. Units are coerced before handlers run. S88 colours are mandatory in every diagram. Source of truth: `.claude/refactor/CONTRACTS.md` §1.
## Topic prefixes ---
Every topic is `<prefix>.<verb>` lowercase. Five prefixes only. ## The six prefixes
```mermaid ```mermaid
flowchart LR flowchart LR
ui[UI / parent / driver]:::neutral ui["UI / parent / driver / function node"]
node[Node]:::tier3 node[Node]
child[Child]:::tier1 child["Child node"]
ext["external consumers"]
ui -->|set.x / cmd.x| node ui -- "set. / cmd. / query." --> node
node -->|evt.x| ui node -. "evt." .-> ui
child -->|data.x| node node -. "evt." .-> ext
node -->|data.x| child child -- "data." --> node
child -->|child.register| node node -- "data." --> child
child <-->|child.register| node
class ui,ext neutral
class node tier3
class child tier1
classDef neutral fill:#dddddd classDef neutral fill:#dddddd
classDef tier3 fill:#50a8d9,color:#000 classDef tier3 fill:#50a8d9,color:#000
classDef tier1 fill:#a9daee,color:#000 classDef tier1 fill:#a9daee,color:#000
``` ```
| Prefix | Direction | Semantics | Examples | ### Inbound (the node accepts on its input)
|---|---|---|---|
| `set.` | inbound | Set a configurable value. **Idempotent**, no side-effects beyond storing the value. | `set.mode`, `set.demand`, `set.position` |
| `cmd.` | inbound | Trigger an action. **Has side-effects** (state transitions, motor commands). | `cmd.startup`, `cmd.shutdown`, `cmd.calibrate`, `cmd.estop` |
| `data.` | bidirectional | Carries measurement / process data. Used by `measurement → parent` and emitters. | `data.pressure`, `data.flow`, `data.temperature` |
| `evt.` | outbound | Announces something happened. Consumer-driven. | `evt.state-change`, `evt.alarm`, `evt.health` |
| `child.` | inbound (parent) | Child node lifecycle. | `child.register` (with legacy alias `registerChild`) |
**Anti-patterns to avoid:** | Prefix | Idempotent | Meaning | Examples |
- ❌ A topic that does two things (`setStartup` to both set a flag *and* trigger startup). Split into `set.` + `cmd.`. |:---|:---|:---|:---|
- ❌ Reusing a `cmd.` topic for both inbound trigger and outbound ack — make a paired `evt.<verb>-complete`. | `set.<noun>` | Yes | Setter. Replaces a state value with the supplied payload. Repeating with the same payload does nothing extra. | `set.mode`, `set.scaling`, `set.demand`, `set.inflow` |
- ❌ Per-node prefixes (`pump.set.demand`). The prefix is the *kind*, not the *target*. | `cmd.<verb>` | No | Imperative action. Triggers a transition or sequence. Repeating triggers it again (or is rejected). | `cmd.startup`, `cmd.shutdown`, `cmd.estop`, `cmd.calibrate` |
| `data.<noun>` | n/a (values flow) | Bulk data input. Sensor readings, measurement values, raw streams. The node consumes them. | `data.measurement`, `data.flow`, `data.pressure` |
| `query.<noun>` | Yes (read-only) | Synchronous query. The node responds on the same msg (or a sibling output). For dashboards / debug. | `query.curves`, `query.cog`, `query.snapshot` |
| `child.<verb>` | n/a (plumbing) | Parent / child plumbing. Routed via Port 2. | `child.register`, `child.unregister` |
## Alias deprecation ### Outbound (the node emits)
Legacy topic names (pre-refactor) are still accepted as aliases. The current alias map per node lives in `src/commands/index.js`. Common aliases: | Prefix | Meaning | Where it appears |
|:---|:---|:---|
| `evt.<noun>` | Event. A fact about something that just happened. Fire-and-forget &mdash; no consumer required. | `msg.topic` on Port 0; also fired on `this.emitter` for sibling modules |
The default measurement output (delta-compressed payload from `outputUtils.formatMsg`) keeps `msg.topic = config.general.name` per existing convention. `evt.*` is for additional event-shaped emissions, not the per-tick measurement stream.
> [!TIP]
> The prefix is the kind, never the target. Don't write `pump.set.demand` &mdash; write `set.demand` and let routing handle which pump. The prefix system says explicitly what the message does; the target is identified by node id, not by topic.
> [!CAUTION]
> One topic, two effects is a bug magnet. A topic like `setStartup` that both sets a flag and triggers startup should be split into `set.<noun>` and `cmd.<verb>`.
---
## Aliases and deprecation
Each `commands/index.js` declares the canonical name as `topic` and lists pre-refactor names in `aliases`. First use of each alias logs a one-time deprecation warning. Aliases are removed in Phase 7 after one release cycle.
```js
{
topic: 'set.mode',
aliases: ['setMode', 'changemode'],
payloadSchema: { type: 'string' },
description: 'Switch the node between auto and manual control modes.',
handler: handlers.setMode,
}
```
### Common alias map
| Canonical | Legacy aliases | | Canonical | Legacy aliases |
|---|---| |:---|:---|
| `set.mode` | `setMode` | | `set.mode` | `setMode`, `changemode` |
| `set.demand` | `Qd`, `setDemand` | | `set.demand` | `Qd`, `setDemand` |
| `cmd.startup` | `execSequence` with `payload.action='startup'` | | `cmd.startup` | `execSequence` (with `payload.action='startup'`) |
| `cmd.shutdown` | `execSequence` with `payload.action='shutdown'` | | `cmd.shutdown` | `execSequence` (with `payload.action='shutdown'`) |
| `child.register` | `registerChild` | | `child.register` | `registerChild` |
| `data.pressure` | `pressure` | | `data.pressure` | `pressure` |
| `data.flow` | `flow` | | `data.flow` | `flow` |
Aliases are logged at debug level on use. Plan is to remove them in a future major version. Update integrations to canonical names. > [!IMPORTANT]
> Update integrations to canonical names before Phase 7 ships. Aliases work today; they will be removed next major release.
## Unit policy ---
Every node declares canonical + output units via `UnitPolicy.declare({canonical, output})`. The command registry coerces incoming `msg.unit` to the canonical unit before the handler runs. Outputs are emitted in the declared output unit (often human-friendly). ## Payload schemas
`payloadSchema.type` accepts six values. Source: `.claude/refactor/CONTRACTS.md` §4.
| Type | Meaning |
|:---|:---|
| `'string'` | `typeof payload === 'string'` |
| `'number'` | `typeof payload === 'number'` |
| `'boolean'` | `typeof payload === 'boolean'` |
| `'object'` | Non-null object. Optional `properties: { key: 'typeName' }` enforces per-key `typeof` (missing keys allowed) |
| `'any'` | Anything passes. Use when handler accepts heterogeneous payloads |
| `'none'` | Trigger-only. Handler invoked regardless of payload. If `msg.payload` is anything but `undefined` / `null`, registry logs a `warn` and still invokes the handler |
---
## Unit coercion (pre-dispatch)
A descriptor for a numeric setter or data topic may declare:
```js
{
topic: 'set.demand',
units: { measure: 'volumeFlowRate', default: 'm3/h' },
payloadSchema: { type: 'number' },
handler: handlers.setDemand,
}
```
```mermaid ```mermaid
flowchart LR flowchart LR
ui[UI message<br/>e.g. 50 m³/h]:::neutral in["Inbound msg &mdash; payload=50, unit='m3/h'"]
coerce[unit coercion<br/>m³/h → m³/s]:::tier1 parse["Extract value+unit &mdash; 3 payload shapes accepted"]
sc[specificClass<br/>canonical m³/s]:::tier3 convert["convert(value).from(unit).to(default)"]
out[output<br/>renders back to m³/h]:::tier2 handler["Handler receives msg.payload = canonical number, msg.unit = units.default"]
ui --> coerce --> sc --> out in --> parse --> convert --> handler
class in,handler neutral
class parse,convert step
classDef neutral fill:#dddddd classDef neutral fill:#dddddd
classDef tier1 fill:#a9daee,color:#000 classDef step fill:#a9daee,color:#000
classDef tier3 fill:#50a8d9,color:#000
classDef tier2 fill:#86bbdd,color:#000
``` ```
### Three accepted payload shapes
| Shape | Example |
|:---|:---|
| Plain number | `msg.payload = 50; msg.unit = 'l/s'` |
| Object with explicit unit | `msg.payload = { value: 50, unit: 'l/s' }` |
| Object without unit (falls back to `msg.unit`) | `msg.payload = { value: 50 }; msg.unit = 'l/s'` |
### Behaviour on unit mismatch
| Situation | What the registry does |
|:---|:---|
| No unit supplied | Silently assume `units.default` |
| Unit recognised + correct measure | Convert and rewrite payload |
| Unit recognised, wrong measure | Log `warn` with accepted-unit list; fall through |
| Unit unrecognised | Log `warn` with accepted-unit list; fall through |
The handler always sees a plain number in `units.default`. Source: `.claude/refactor/CONTRACTS.md` §4 ("Determine the unit-of-record").
---
## S88 colour palette
Every Mermaid diagram, every Node-RED node editor colour, every FlowFuse dashboard group uses this palette. Source: `.claude/rules/node-red-flow-layout.md` §14.
| Hex | S88 level | Used by |
|:---|:---|:---|
| `#0f52a5` | Area | Reserved &mdash; not used yet |
| `#0c99d9` | Process Cell | pumpingStation |
| `#50a8d9` | Unit | MGC, VGC, reactor, settler, monster |
| `#86bbdd` | Equipment Module | rotatingMachine, valve, diffuser |
| `#a9daee` | Control Module | measurement |
| `#dddddd` | Utility / neutral | dashboardAPI, helper functions |
> [!WARNING]
> Known palette outliers (pending cleanup, tracked in `.claude/refactor/OPEN_QUESTIONS.md`):
> - `settler` editor colour is `#e4a363` (orange) &mdash; should be `#50a8d9`.
> - `monster` editor colour is `#4f8582` (teal) &mdash; should be `#50a8d9`.
> - `diffuser` registers under category `'wbd typical'` instead of `'EVOLV'`.
>
> Wiki diagrams use the correct S88 colour regardless of the editor mismatch.
---
## Measurement key shape
`MeasurementContainer` stores values under composite keys:
```
<type>.<variant>.<position>.<childId>
| | | |
| | | +-- child id (or 'default' for internal computations)
| | +------------- 'upstream' / 'downstream' / 'atequipment' / ... (always lowercase in keys)
| +------------------------ 'measured' / 'predicted' / 'setpoint' / 'min' / 'max'
+-------------------------------- 'flow' / 'pressure' / 'power' / 'temperature' / 'level'
```
### Examples
| Key | Meaning |
|:---|:---|
| `flow.measured.downstream.dashboard-sim-downstream` | Externally measured downstream flow |
| `flow.predicted.downstream.default` | The node's own prediction |
| `power.measured.atequipment.default` | Measured power at the equipment |
| `pressure.measured.upstream.<childId>` | Pressure from a specific measurement child |
> [!WARNING]
> `position` is always lowercase in keys. The configuration form may use mixed case (`atEquipment`); the container normalises. Don't rely on the casing the form shows you.
---
## Status badge
`statusBadge.compose(state)` returns `{fill, shape, text}` for `node.status(...)`.
```js
const { statusBadge } = require('generalFunctions');
statusBadge.compose(['OK', `flow=${flow.toFixed(1)} m3/h`])
statusBadge.error(message)
statusBadge.idle(label)
```
| `fill` | `shape` | Meaning |
|:---|:---|:---|
| `blue` | `dot` | Normal / running |
| `green` | `dot` | Success / running optimally |
| `yellow` | `ring` | Degraded &mdash; attention needed |
| `red` | `ring` | Fault &mdash; operator action required |
| `grey` | `dot` | Initialising / no data yet |
> [!IMPORTANT]
> Badges live in the domain, not the adapter. `nodeClass` calls `this.source.getStatusBadge()` once per second; the domain owns the shape. Source: `.claude/refactor/CONTRACTS.md` §7.
---
## HealthStatus
A standardised shape for nodes that compute prediction quality, drift, or general health. Source: `.claude/refactor/CONTRACTS.md` §9.
```json
{
"level": 1,
"flags": ["pressure_init_warming"],
"message": "warmup phase",
"source": "rotatingMachine#pump-A"
}
```
| Field | Type | Meaning |
|:---|:---|:---|
| `level` | `0 \| 1 \| 2 \| 3` | 0 = fine, 3 = unusable |
| `flags` | `string[]` | Machine-readable tags (e.g. `no_pressure_input`) |
| `message` | `string` | Single-line human summary |
| `source` | `string \| null` | `<nodeType>#<id>` &mdash; for routing UI / alarm correlation |
Helpers compose multiple sub-statuses (flow drift + power drift + pressure init) into one node-level status.
---
## Canonical units (used in code)
Every node declares its `UnitPolicy`: what canonical (internal) unit it uses and what output unit to render to. Source: `.claude/refactor/CONTRACTS.md` §6.
| Quantity | Canonical (internal) | Common output | | Quantity | Canonical (internal) | Common output |
|---|---|---| |:---|:---|:---|
| Flow | `m3/s` | `m3/h`, `l/s`, `gpm` | | Flow | `m3/s` | `m3/h`, `l/s`, `gpm` |
| Pressure | `Pa` | `bar`, `mbar`, `kPa` | | Pressure | `Pa` | `bar`, `mbar`, `kPa` |
| Power | `W` | `kW`, `MW` | | Power | `W` | `kW`, `MW` |
@@ -82,104 +267,26 @@ flowchart LR
| Level | `m` | `m`, `cm` | | Level | `m` | `m`, `cm` |
| Volume | `m3` | `m3`, `l` | | Volume | `m3` | `m3`, `l` |
**Rule:** anywhere in `specificClass`, treat values as canonical. Conversion happens at the boundary (input coercion + output formatting). > [!TIP]
> Inside `specificClass`, treat values as canonical. Conversion happens at the boundary: input coercion by the commands registry; output formatting by `outputUtils`.
## S88 colour palette ### Dual access form
```mermaid `UnitPolicy` exposes each accessor as both a method and a frozen property bag.
flowchart TB
A[Area<br/>#0f52a5]:::area
PC[Process Cell<br/>#0c99d9]:::pc
UN[Unit<br/>#50a8d9]:::unit
EM[Equipment Module<br/>#86bbdd]:::equip
CM[Control Module<br/>#a9daee]:::ctrl
UT[Utility / neutral<br/>#dddddd]:::neutral
A --> PC --> UN --> EM --> CM ```js
UT -.- A policy.canonical('flow') // 'm3/s' (method form)
policy.canonical.flow // 'm3/s' (property form &mdash; preferred in hot paths)
classDef area fill:#0f52a5,color:#fff policy.output.pressure // 'mbar'
classDef pc fill:#0c99d9,color:#fff
classDef unit fill:#50a8d9,color:#000
classDef equip fill:#86bbdd,color:#000
classDef ctrl fill:#a9daee,color:#000
classDef neutral fill:#dddddd,color:#000
``` ```
| Hex | S88 level | Used by | ---
|---|---|---|
| `#0f52a5` | Area | (reserved — not in use yet) |
| `#0c99d9` | Process Cell | pumpingStation |
| `#50a8d9` | Unit | machineGroupControl, valveGroupControl, reactor, settler, monster |
| `#86bbdd` | Equipment Module | rotatingMachine, valve, diffuser |
| `#a9daee` | Control Module | measurement |
| `#dddddd` | Utility / neutral | dashboardAPI, helper function nodes |
**Rule:** every Mermaid diagram in this wiki, every Node-RED node's editor colour, and every dashboard grouping uses this palette. Source of truth: `.claude/rules/node-red-flow-layout.md` §14.
**Known outliers** (pending cleanup, tracked in OPEN_QUESTIONS.md):
- `settler` editor colour is `#e4a363` (orange) — should be `#50a8d9`.
- `monster` editor colour is `#4f8582` (teal) — should be `#50a8d9`.
- `diffuser` editor colour was missing pre-refactor; now `#86bbdd`.
- `dashboardAPI` registers under category `'wbd typical'` instead of `'EVOLV'`.
## Measurement key shape
The `MeasurementContainer` stores values under composite keys:
```
<type>.<variant>.<position>.<childId>
```
| Segment | Examples |
|---|---|
| `type` | `flow`, `pressure`, `power`, `temperature`, `level` |
| `variant` | `measured`, `predicted`, `setpoint`, `min`, `max` |
| `position` | `upstream`, `downstream`, `atequipment`, `inlet`, `outlet` (always lowercase in keys) |
| `childId` | The registering child's id, OR `default` for internal computations |
Examples:
- `flow.measured.downstream.dashboard-sim-downstream` — externally measured downstream flow.
- `flow.predicted.downstream.default` — node's own prediction.
- `power.measured.atequipment.default` — measured power at the equipment.
- `pressure.measured.upstream.<childId>` — pressure from a specific measurement child.
**Gotcha:** `position` is **always lowercase in keys**. The configuration form may use mixed case (`atEquipment`); the container normalises.
## Status badge
`statusBadge.compose(state)` returns `{fill, shape, text}` for `node.status(...)`:
| level | shape | fill | meaning |
|---|---|---|---|
| `info` | dot | blue | normal operation |
| `success` | dot | green | success / running optimally |
| `warning` | ring | yellow | degraded, attention needed |
| `error` | ring | red | fault, operator action required |
| `pending` | dot | grey | initialising / no data yet |
Composer reads from `HealthStatus.level` (03) — kept centralised so all nodes show consistent badges.
## HealthStatus shape
```json
{
"level": 0,
"flags": ["pressure_init_warming"],
"message": "warmup phase",
"source": "rotatingMachine#pump-A"
}
```
| Field | Range | Meaning |
|---|---|---|
| `level` | 0..3 | 0 = healthy, 1 = degraded, 2 = warning, 3 = error |
| `flags` | string[] | Machine-readable reason codes. |
| `message` | string | Human-readable summary (one line). |
| `source` | string | `<nodeType>#<id>` — for routing UI / alarm correlation. |
## Related pages ## Related pages
- [Architecture](Architecture) — generalFunctions API surface | Page | Why |
- [Telemetry](Telemetry) — Port-1 InfluxDB schema (where these conventions appear in stored data) |:---|:---|
- [Topology-Patterns](Topology-Patterns) — what topics flow where | [Architecture](Architecture) | Where these conventions are implemented in code |
| [Telemetry](Telemetry) | What these keys look like in InfluxDB |
| [Topology Patterns](Topology-Patterns) | Which topics flow between which nodes |
| [Glossary](Glossary) | Domain terms used here |

@@ -1,32 +1,49 @@
# Topology Patterns # Topology Patterns
> **Reflects code as of `9ab9f6b` · regenerated `2026-05-11`** ![code-ref](https://img.shields.io/badge/code--ref-9ab9f6b-blue)
![verified](https://img.shields.io/badge/edges-verified_against_configure()-brightgreen)
Typical plant configurations and how nodes wire together. Each pattern is **verified** against the corresponding nodes' `configure()` declarations. > [!NOTE]
> Five canonical plant configurations and one worked example that combines them. Every edge in every diagram was checked against the parent's `configure()` declaration in source. Use these as templates when wiring your own plant.
## Pattern 1 — Pumping station with grouped pumps ---
The canonical wet-well lift station. One basin, one demand controller (`pumpingStation`), one load-sharing coordinator (`machineGroupControl`), N pumps. Level + flow measurements feed the basin model. ## Pattern index
| Pattern | When to use it |
|:---|:---|
| [1. Pumping station with grouped pumps](#1-pumping-station-with-grouped-pumps) | Lift station, single basin, N pumps load-shared |
| [2. Reactor + diffuser + settler train](#2-reactor--diffuser--settler-train) | Biological treatment line |
| [3. Valve group on a distribution manifold](#3-valve-group-on-a-distribution-manifold) | Multi-valve flow split with upstream flow context |
| [4. Composite sampling](#4-composite-sampling) | Flow-proportional grab samples for lab analysis |
| [5. Dashboard provisioning](#5-dashboard-provisioning) | Auto-generated Grafana dashboards |
| [Worked example &mdash; small WWTP](#worked-example--small-wwtp) | All five patterns combined |
---
## 1. Pumping station with grouped pumps
The canonical wet-well lift station: one basin model, one demand controller (`pumpingStation`), one load-sharing coordinator (`machineGroupControl`), N pumps (`rotatingMachine` &times; N), measurements for level + flow + per-pump pressure.
```mermaid ```mermaid
flowchart TB flowchart TB
subgraph PC["Process Cell"] subgraph PC["Process Cell"]
ps[pumpingStation]:::pc ps[pumpingStation]
end end
subgraph UN["Unit"] subgraph UN["Unit"]
mgc[machineGroupControl]:::unit mgc[machineGroupControl]
end end
subgraph EM["Equipment"] subgraph EM["Equipment Module"]
rmA[rotatingMachine A]:::equip rmA[rotatingMachine A]
rmB[rotatingMachine B]:::equip rmB[rotatingMachine B]
rmC[rotatingMachine C]:::equip rmC[rotatingMachine C]
end end
subgraph CM["Control Module"] subgraph CM["Control Module"]
ml[measurement: level]:::ctrl ml["measurement &mdash; level"]
mfin[measurement: inflow]:::ctrl mfin["measurement &mdash; inflow"]
mpA[measurement: pressure A]:::ctrl mpA["measurement &mdash; pressure A"]
mpB[measurement: pressure B]:::ctrl mpB["measurement &mdash; pressure B"]
mpC[measurement: pressure C]:::ctrl mpC["measurement &mdash; pressure C"]
end end
ps --> mgc ps --> mgc
@@ -40,82 +57,102 @@ flowchart TB
mpB -. data .-> rmB mpB -. data .-> rmB
mpC -. data .-> rmC mpC -. data .-> rmC
classDef pc fill:#0c99d9,color:#fff class ps pc
classDef unit fill:#50a8d9,color:#000 class mgc unit
classDef equip fill:#86bbdd,color:#000 class rmA,rmB,rmC equip
classDef ctrl fill:#a9daee,color:#000 class ml,mfin,mpA,mpB,mpC ctrl
classDef pc fill:#0c99d9,color:#fff,stroke:#075a82,stroke-width:2px
classDef unit fill:#50a8d9,color:#000,stroke:#2c7ba8,stroke-width:2px
classDef equip fill:#86bbdd,color:#000,stroke:#5a90b2,stroke-width:2px
classDef ctrl fill:#a9daee,color:#000,stroke:#76b7d4,stroke-width:2px
``` ```
**Data flow:** ### Data flow
- `pumpingStation` computes basin volume + level dynamics from inflow/outflow measurements.
- `pumpingStation` emits a demand setpoint downstream to `machineGroupControl` on its Port 0 or via `set.demand`.
- `machineGroupControl` solves a per-pump operating point using each pump's characteristic curve + measured pressure, sends `set.setpoint` to each `rotatingMachine`.
- Each `rotatingMachine` runs its own FSM (idle/warmingup/operational/coolingdown/emergencystop) and predicts flow/power from pressure + speed.
**Notes:** | Stage | What happens |
- For a single-pump station, `pumpingStation` can register `rotatingMachine` directly (skip the MGC) — `pumpingStation`'s `configure()` accepts `machine` as a child softwareType. |:---|:---|
- For two stations in series, the downstream PS can register the upstream PS as a `pumpingstation` softwareType source. | Basin integration | `pumpingStation` integrates basin volume from inflow / outflow rates |
| Demand computation | `pumpingStation` computes a demand setpoint and dispatches it to `machineGroupControl` |
| Per-pump operating point | `machineGroupControl` solves a per-pump operating point using each pump's characteristic curve plus measured upstream pressure |
| Pump dispatch | Each `rotatingMachine` runs its own FSM (`idle` &rarr; `warmingup` &rarr; `operational` &rarr; `coolingdown` &rarr; `emergencystop`, plus `accelerating` / `decelerating`) and predicts flow + power from speed + pressure |
## Pattern 2 — Reactor / settler train with aeration ### Variants
Biological treatment line. Reactor runs ASM kinetics, diffuser drives O₂ transfer, settler clarifies effluent and returns sludge via a return pump. | Variant | How to wire |
|:---|:---|
| Single pump (no MGC) | `pumpingStation.configure()` accepts `machine` directly &mdash; skip the MGC and parent the `rotatingMachine` under `pumpingStation` |
| Cascaded stations | `pumpingStation.configure()` accepts `pumpingstation` as a child &mdash; downstream PS registers upstream PS to read its predicted outflow |
---
## 2. Reactor + diffuser + settler train
Biological treatment line. `reactor` runs ASM kinetics (CSTR or PFR engine, set via `config.reactor_type`). `diffuser` injects OTR. `settler` clarifies the effluent and drives a return pump.
```mermaid ```mermaid
flowchart TB flowchart TB
subgraph UN["Unit"] subgraph UN["Unit"]
reactor[reactor]:::unit reactor[reactor]
settler[settler]:::unit settler[settler]
end end
subgraph EM["Equipment"] subgraph EM["Equipment Module"]
diff[diffuser]:::equip diff[diffuser]
rp[rotatingMachine<br/>return pump]:::equip rp["rotatingMachine &mdash; return pump"]
end end
subgraph CM["Control Module"] subgraph CM["Control Module"]
mt[measurement: temperature]:::ctrl mt["measurement &mdash; temperature"]
mdo[measurement: dissolved O₂]:::ctrl mdo["measurement &mdash; dissolved O2"]
mts[measurement: TSS]:::ctrl mts["measurement &mdash; TSS"]
end end
reactor ==stateChange==> settler reactor ==stateChange==> settler
diff -. OTR data .-> reactor diff -. OTR data .-> reactor
settler -->|return pump child| rp settler -->|return pump| rp
mt -. data .-> reactor mt -. data .-> reactor
mdo -. data .-> reactor mdo -. data .-> reactor
mts -. data .-> settler mts -. data .-> settler
mdo -. data .-> diff mdo -. data .-> diff
classDef unit fill:#50a8d9,color:#000 class reactor,settler unit
classDef equip fill:#86bbdd,color:#000 class diff,rp equip
classDef ctrl fill:#a9daee,color:#000 class mt,mdo,mts ctrl
classDef unit fill:#50a8d9,color:#000,stroke:#2c7ba8,stroke-width:2px
classDef equip fill:#86bbdd,color:#000,stroke:#5a90b2,stroke-width:2px
classDef ctrl fill:#a9daee,color:#000,stroke:#76b7d4,stroke-width:2px
``` ```
**Data flow:** ### Two non-standard wirings
- `reactor.configure()` registers `measurement` (temperature, DO) and upstream `reactor` (for chained tanks).
- `diffuser` emits `data.otr` on its emitter; reactor subscribes via `emitter.on('otr', …)`**not** a child registration, just a data subscription.
- `reactor` emits `stateChange` after every kinetics step. `settler._connectReactor` subscribes via `emitter.on('stateChange', …)` and pulls effluent composition.
- `settler.configure()` accepts `reactor` (the upstream), `machine` (return pump), and `measurement` children.
**Notes:** > [!IMPORTANT]
- Reactor supports two kinetics engines: CSTR (continuous-stirred tank) and PFR (plug-flow). Set via `config.reactor_type`. > `diffuser` &rarr; `reactor` is data-only. Diffuser fires `data.otr` on its emitter; reactor subscribes via `emitter.on('otr', ...)`. There is no `child.register` handshake between them. See `nodes/reactor/src/specificClass.js` `configure()`.
- DO setpoint feedback (DO measurement → diffuser airflow) is not wired automatically — connect via a small control function or use a `valveGroupControl` upstream of an airflow valve.
## Pattern 3 — Valve group on a distribution manifold > [!IMPORTANT]
> `reactor` &rarr; `settler` is a `stateChange` subscription, not a parent / child edge. Settler's `_connectReactor` attaches `emitter.on('stateChange', ...)` to pull effluent composition from the upstream reactor. The `reactor` softwareType is registered as a child of settler even though the reactor is semantically upstream.
Multi-valve flow distribution. VGC computes per-valve K_v shares to satisfy a target distribution while respecting upstream flow availability. > [!CAUTION]
> DO setpoint feedback is not automatic. A measured-DO &rarr; diffuser-airflow loop must be closed externally (a function node) or via a `valveGroupControl` upstream of an airflow valve.
---
## 3. Valve group on a distribution manifold
Multi-valve flow distribution. `valveGroupControl` computes per-valve K_v shares to satisfy a target split while respecting upstream flow availability.
```mermaid ```mermaid
flowchart TB flowchart TB
subgraph PC["Process Cell"] subgraph PC["Process Cell"]
ps[pumpingStation<br/>upstream flow source]:::pc ps["pumpingStation &mdash; upstream flow source"]
end end
subgraph UN["Unit"] subgraph UN["Unit"]
vgc[valveGroupControl]:::unit vgc[valveGroupControl]
end end
subgraph EM["Equipment"] subgraph EM["Equipment Module"]
vA[valve A]:::equip vA[valve A]
vB[valve B]:::equip vB[valve B]
vC[valve C]:::equip vC[valve C]
end end
ps -. flow source .-> vgc ps -. flow source .-> vgc
@@ -123,95 +160,125 @@ flowchart TB
vgc --> vB vgc --> vB
vgc --> vC vgc --> vC
classDef pc fill:#0c99d9,color:#fff class ps pc
classDef unit fill:#50a8d9,color:#000 class vgc unit
classDef equip fill:#86bbdd,color:#000 class vA,vB,vC equip
classDef pc fill:#0c99d9,color:#fff,stroke:#075a82,stroke-width:2px
classDef unit fill:#50a8d9,color:#000,stroke:#2c7ba8,stroke-width:2px
classDef equip fill:#86bbdd,color:#000,stroke:#5a90b2,stroke-width:2px
``` ```
**Important detail:** `valveGroupControl.configure()` registers four extra softwareTypes — `machine`, `machinegroup`, `pumpingstation`, `valvegroupcontrol`**not as S88 children** but as **flow sources**. VGC uses them to read upstream flow availability when computing per-valve splits. The arrow above is `child.register` from pumpingStation to vgc; the semantic relationship is "VGC knows about this upstream flow producer", not "VGC controls pumpingStation". > [!IMPORTANT]
> VGC's child types are unusual. `valveGroupControl.configure()` registers five softwareTypes:
> - `valve` &mdash; actual S88 child relationship (VGC controls these)
> - `machine`, `machinegroup`, `pumpingstation`, `valvegroupcontrol` &mdash; flow sources. VGC reads upstream flow availability when computing splits. Semantic is "VGC knows about this flow producer", not "VGC controls it".
>
> See `nodes/valveGroupControl/src/specificClass.js` lines 13&ndash;49.
## Pattern 4 — Composite sampling ---
`monster` runs a proportional sampling program — accumulates samples in a bucket based on integrated flow. Used as a virtual sensor for downstream lab analysis. ## 4. Composite sampling
Virtual sensor for downstream lab analysis. `monster` accumulates samples in a bucket based on integrated flow &mdash; a flow-proportional grab sample.
```mermaid ```mermaid
flowchart TB flowchart TB
subgraph UN["Unit"] subgraph UN["Unit"]
monster[monster]:::unit monster[monster]
end end
subgraph CM["Control Module"] subgraph CM["Control Module"]
mflow[measurement: flow<br/>assetType MUST be 'flow']:::ctrl mflow["measurement &mdash; flow (assetType MUST be 'flow')"]
mq[measurement: any quality<br/>e.g. NH, COD]:::ctrl mq["measurement &mdash; any quality (e.g. NH4, COD)"]
end end
mflow -. data .-> monster mflow -. data .-> monster
mq -. data .-> monster mq -. data .-> monster
classDef unit fill:#50a8d9,color:#000 class monster unit
classDef ctrl fill:#a9daee,color:#000 class mflow,mq ctrl
classDef unit fill:#50a8d9,color:#000,stroke:#2c7ba8,stroke-width:2px
classDef ctrl fill:#a9daee,color:#000,stroke:#76b7d4,stroke-width:2px
``` ```
**Gotchas:** > [!WARNING]
- `measurement.config.asset.type` MUST be `"flow"` exactly — `"flow-electromagnetic"` or any sub-type is silently ignored by monster's child router. > Two gotchas:
- `monster.config.constraints.flowmeter` exists in the schema but is **not forwarded** by `buildDomainConfig` — toggling proportional-vs-time mode has no effect at runtime. (Tracked in OPEN_QUESTIONS.md.) > 1. `measurement.config.asset.type` must be exactly `"flow"`. A value like `"flow-electromagnetic"` is silently ignored by monster's child router.
> 2. `monster.config.constraints.flowmeter` exists in the schema but is not forwarded by `buildDomainConfig`. Toggling proportional-vs-time mode has no runtime effect. Tracked in `.claude/refactor/OPEN_QUESTIONS.md`.
## Pattern 5 — Dashboard provisioning ---
`dashboardAPI` doesn't operate on data — it generates Grafana dashboards. Any node can register with `dashboardAPI` via `child.register`; dashboardAPI then composes a dashboard JSON from the node's softwareType + measurements and POSTs to Grafana's HTTP API. ## 5. Dashboard provisioning
`dashboardAPI` doesn't operate on data &mdash; it generates Grafana dashboards. Any node registers via `child.register`; dashboardAPI composes a dashboard JSON from softwareType plus measurements and POSTs to Grafana's HTTP API.
```mermaid ```mermaid
flowchart LR flowchart LR
subgraph EVOLV["EVOLV process nodes"] subgraph EVOLV["EVOLV process nodes (any softwareType)"]
ps[pumpingStation]:::pc direction TB
mgc[machineGroupControl]:::unit ps[pumpingStation]
rm[rotatingMachine]:::equip mgc[machineGroupControl]
rm[rotatingMachine]
end end
subgraph UT["Utility"] subgraph UT["Utility"]
dash[dashboardAPI]:::util dash[dashboardAPI]
end end
grafana[(Grafana<br/>HTTP API)]:::ext grafana[("Grafana HTTP API")]
ps -. child.register .-> dash ps -. child.register .-> dash
mgc -. child.register .-> dash mgc -. child.register .-> dash
rm -. child.register .-> dash rm -. child.register .-> dash
dash -->|POST /api/dashboards/db| grafana dash ==>|POST /api/dashboards/db| grafana
classDef pc fill:#0c99d9,color:#fff class ps pc
classDef unit fill:#50a8d9,color:#000 class mgc unit
classDef equip fill:#86bbdd,color:#000 class rm equip
classDef util fill:#dddddd,color:#000 class dash util
classDef ext fill:#fff2cc,color:#000 class grafana ext
classDef pc fill:#0c99d9,color:#fff,stroke:#075a82,stroke-width:2px
classDef unit fill:#50a8d9,color:#000,stroke:#2c7ba8,stroke-width:2px
classDef equip fill:#86bbdd,color:#000,stroke:#5a90b2,stroke-width:2px
classDef util fill:#dddddd,color:#000,stroke:#a8a8a8,stroke-width:2px
classDef ext fill:#fff2cc,color:#000,stroke:#aa8400,stroke-width:2px
``` ```
**Notes:** | Behaviour | Detail |
- `dashboardAPI` is the one node in the platform that doesn't extend `BaseDomain` (it's a passive HTTP bridge — see OPEN_QUESTIONS.md for the deferral decision). |:---|:---|
- The `meta` field of dashboardAPI's outbound msg carries `{nodeId, softwareType, uid, title}` for correlating responses. | What it accepts | Any softwareType on `child.register` |
| What it emits | One HTTP POST per registered child, payload from `nodes/dashboardAPI/src/config/templates/<softwareType>.json` |
| Auth | Bearer token in `config.grafanaConnector.bearerToken` (when set) |
| `meta` envelope | `{nodeId, softwareType, uid, title}` for correlating responses |
| Architecture variance | The one node in the platform that does not extend `BaseDomain`. Documented in `.claude/refactor/OPEN_QUESTIONS.md` |
## Putting it all together — example plant ---
A small WWTP combining all patterns: ## Worked example &mdash; small WWTP
All five patterns combined.
```mermaid ```mermaid
flowchart TB flowchart TB
subgraph PC["Process Cell"] subgraph PC["Process Cell"]
ps1[pumpingStation<br/>inlet lift]:::pc ps1["pumpingStation &mdash; inlet lift"]
ps2[pumpingStation<br/>RAS pumping]:::pc ps2["pumpingStation &mdash; RAS pumping"]
end end
subgraph UN["Unit"] subgraph UN["Unit"]
mgc1[MGC inlet]:::unit mgc1["MGC inlet"]
mgc2[MGC RAS]:::unit mgc2["MGC RAS"]
vgc[VGC effluent split]:::unit vgc["VGC effluent split"]
r1[reactor aerobic]:::unit r1["reactor aerobic"]
s1[settler]:::unit s1["settler"]
mon[monster<br/>composite sampler]:::unit mon["monster &mdash; composite sampler"]
end end
subgraph EM["Equipment"] subgraph EM["Equipment Module"]
rm1[pump A]:::equip rm1["pump A"]
rm2[pump B]:::equip rm2["pump B"]
rm3[RAS pump]:::equip rm3["RAS pump"]
d1[diffuser]:::equip d1["diffuser"]
v1[valve 1]:::equip v1["valve 1"]
v2[valve 2]:::equip v2["valve 2"]
end end
ps1 --> mgc1 ps1 --> mgc1
@@ -228,21 +295,45 @@ flowchart TB
vgc --> v1 vgc --> v1
vgc --> v2 vgc --> v2
classDef pc fill:#0c99d9,color:#fff class ps1,ps2 pc
classDef unit fill:#50a8d9,color:#000 class mgc1,mgc2,vgc,r1,s1,mon unit
classDef equip fill:#86bbdd,color:#000 class rm1,rm2,rm3,d1,v1,v2 equip
classDef pc fill:#0c99d9,color:#fff,stroke:#075a82,stroke-width:2px
classDef unit fill:#50a8d9,color:#000,stroke:#2c7ba8,stroke-width:2px
classDef equip fill:#86bbdd,color:#000,stroke:#5a90b2,stroke-width:2px
``` ```
This is the kind of diagram each `wiki/Home.md` per node should be able to fit into — every edge is reproducible from a `configure()` declaration. | Sub-pattern | Recognise it |
|:---|:---|
| Pumping station with grouped pumps (&times;2) | `ps1 -> mgc1 -> {rm1, rm2}` and `ps2 -> mgc2 -> rm3` |
| Reactor + settler train | `r1 ==stateChange==> s1` plus `d1 -. OTR .-> r1` |
| Valve group on flow source | `ps2 -. flow source .-> vgc -> {v1, v2}` |
| Settler return pump | `s1 -> rm3` |
| Composite sampling | `mon` (would be wired to inflow + quality measurements not drawn) |
Every edge here is reproducible from one of the patterns above.
---
## Anti-patterns ## Anti-patterns
-`pumpingStation → valveGroupControl` as a parent/child edge. PS does not register VGC. VGC registers PS as a *flow source*; the edge goes the other way semantically. > [!CAUTION]
- ❌ A `diffuser → reactor` child registration. Diffuser emits OTR via its emitter; reactor subscribes. No `child.register` handshake. > `pumpingStation` &rarr; `valveGroupControl` as a parent / child edge. PS does not register VGC. VGC registers PS as a flow source &mdash; the edge goes the other way semantically.
-`measurement` parented under `dashboardAPI`. dashboardAPI accepts any node for Grafana provisioning, but `measurement` registers with the **process** node it's monitoring, not with dashboardAPI.
> [!CAUTION]
> `diffuser` &rarr; `reactor` as a child registration. Diffuser emits OTR via its emitter; reactor subscribes via `emitter.on`. No `child.register` handshake.
> [!CAUTION]
> `measurement` parented under `dashboardAPI`. dashboardAPI accepts any node for Grafana provisioning, but `measurement` should register with the process node it is monitoring, not with dashboardAPI.
---
## Related pages ## Related pages
- [Home](Home) — top-level node map | Page | Why |
- [Architecture](Architecture) — 3-tier code structure + generalFunctions API |:---|:---|
- [Topic-Conventions](Topic-Conventions) — what topics flow between nodes | [Home](Home) | Top-level node map |
| [Architecture](Architecture) | Three-tier code + generalFunctions API |
| [Topic Conventions](Topic-Conventions) | What topics flow on each edge |
| [Telemetry](Telemetry) | Port 0 / 1 / 2 InfluxDB layout |

@@ -1,10 +1,12 @@
### EVOLV Wiki ### EVOLV Wiki
**Start here** **Start here**
- [Home](Home) - [Home](Home)
- [Getting Started](Getting-Started) - [Getting Started](Getting-Started)
**Reference** **Reference**
- [Architecture](Architecture) - [Architecture](Architecture)
- [Topology Patterns](Topology-Patterns) - [Topology Patterns](Topology-Patterns)
- [Topic Conventions](Topic-Conventions) - [Topic Conventions](Topic-Conventions)
@@ -12,6 +14,7 @@
- [Glossary](Glossary) - [Glossary](Glossary)
**Per-node wikis** **Per-node wikis**
- [pumpingStation](https://gitea.wbd-rd.nl/RnD/pumpingStation/wiki/Home) - [pumpingStation](https://gitea.wbd-rd.nl/RnD/pumpingStation/wiki/Home)
- [machineGroupControl](https://gitea.wbd-rd.nl/RnD/machineGroupControl/wiki/Home) - [machineGroupControl](https://gitea.wbd-rd.nl/RnD/machineGroupControl/wiki/Home)
- [valveGroupControl](https://gitea.wbd-rd.nl/RnD/valveGroupControl/wiki/Home) - [valveGroupControl](https://gitea.wbd-rd.nl/RnD/valveGroupControl/wiki/Home)
@@ -25,21 +28,39 @@
- [dashboardAPI](https://gitea.wbd-rd.nl/RnD/dashboardAPI/wiki/Home) - [dashboardAPI](https://gitea.wbd-rd.nl/RnD/dashboardAPI/wiki/Home)
- [generalFunctions](https://gitea.wbd-rd.nl/RnD/generalFunctions/wiki/Home) - [generalFunctions](https://gitea.wbd-rd.nl/RnD/generalFunctions/wiki/Home)
**Concepts** (domain knowledge) **Domain concepts**
- [ASM models](Concept-ASM-Models)
- [PID control theory](Concept-PID-Control-Theory)
- [Pump affinity laws](Concept-Pump-Affinity-Laws)
- [Settling models](Concept-Settling-Models)
- [Signal processing](Concept-Signal-Processing-Sensors)
- [InfluxDB schema](Concept-InfluxDB-Schema-Design)
- [Compliance NL](Concept-Wastewater-Compliance-NL)
- [OT security](Concept-OT-Security-IEC62443)
**Findings** (algorithm proofs) - [ASM Models](Concept-ASM-Models)
- [BEP gravitation](Finding-BEP-Gravitation-Proof) - [PID Control Theory](Concept-PID-Control-Theory)
- [Curve non-convexity](Finding-Curve-Non-Convexity) - [Pump Affinity Laws](Concept-Pump-Affinity-Laws)
- [NCog behaviour](Finding-NCog-Behavior) - [Settling Models](Concept-Settling-Models)
- [Pump switching stability](Finding-Pump-Switching-Stability) - [Signal Processing — Sensors](Concept-Signal-Processing-Sensors)
- [InfluxDB Schema Design](Concept-InfluxDB-Schema-Design)
- [Wastewater Compliance NL](Concept-Wastewater-Compliance-NL)
- [OT Security IEC 62443](Concept-OT-Security-IEC62443)
**Operations findings**
- [BEP Gravitation Proof](Finding-BEP-Gravitation-Proof)
- [Curve Non-Convexity](Finding-Curve-Non-Convexity)
- [NCog Behaviour](Finding-NCog-Behavior)
- [Pump Switching Stability](Finding-Pump-Switching-Stability)
**Node-RED / FlowFuse manuals**
- [Manual Index](Manual-NodeRED-INDEX)
- [Runtime — Node.js](Manual-NodeRED-Runtime-Node-Js)
- [Function Node Patterns](Manual-NodeRED-Function-Node-Patterns)
- [Messages and Editor Structure](Manual-NodeRED-Messages-And-Editor-Structure)
- [FlowFuse ui-chart](Manual-NodeRED-Flowfuse-Ui-Chart-Manual)
- [FlowFuse ui-button](Manual-NodeRED-Flowfuse-Ui-Button-Manual)
- [FlowFuse ui-gauge](Manual-NodeRED-Flowfuse-Ui-Gauge-Manual)
- [FlowFuse ui-text](Manual-NodeRED-Flowfuse-Ui-Text-Manual)
- [FlowFuse ui-template](Manual-NodeRED-Flowfuse-Ui-Template-Manual)
- [FlowFuse ui-config](Manual-NodeRED-Flowfuse-Ui-Config-Manual)
- [Dashboard Layout](Manual-NodeRED-Flowfuse-Dashboard-Layout-Manual)
- [Widgets Catalog](Manual-NodeRED-Flowfuse-Widgets-Catalog)
**Archive** **Archive**
- [Archive index](Archive)
- [Archive](Archive)