Refactor master wiki: 7 new master pages + accurate Home + archive cleanup

Replaces the prior 9 Architecture-* pre-refactor planning docs and the
broken Home with a complete visual-first redesign. All Mermaid diagrams
have been verified against the actual configure() declarations in each
node's specificClass.js — no fabricated edges.

New master pages:
- Home — accurate platform overview, full live-nodes table, concept
  index, refactor status
- Architecture — 3-tier code structure, generalFunctions API surface
  (12-row), output ports, child-registration sequence
- Topology-Patterns — 5 verified plant configurations with worked
  examples (PS+MGC+pumps, reactor/settler train, VGC manifold,
  composite sampling, dashboard provisioning)
- Topic-Conventions — set./cmd./evt./data./child. semantics, unit
  policy, S88 palette, measurement key shape, status badge, HealthStatus
- Telemetry — Port 0/1/2 contracts, InfluxDB line protocol layout,
  FlowFuse chart wiring, Grafana provisioning
- Getting-Started — clone/install/Docker/local Node-RED setup
- Glossary — S88, EVOLV runtime, WWTP, pumps, control, project terms
- _Sidebar.md — gitea wiki navigation

Concept/finding/manual pages: flattened from source wiki/concepts,
wiki/findings, wiki/manuals/node-red with proper prefixes.

Pre-existing 9 wiki pages (Architecture-*, AI-assisted coding) moved to
Archive- prefix with the archive banner stamped on top. 20 source-tree
archive pages mirrored as Archive-Source-* for completeness.
2026-05-11 21:49:49 +02:00
parent 7b79def045
commit 48a2e509c2
62 changed files with 6921 additions and 51 deletions

170
Architecture.md Executable file

@@ -0,0 +1,170 @@
# Architecture
> **Reflects code as of `9ab9f6b` · regenerated `2026-05-11`**
How every EVOLV node is structured, and what the shared `generalFunctions` library provides.
## The 3-tier node pattern
```mermaid
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
classDef tier2 fill:#86bbdd,color:#000
classDef tier3 fill:#50a8d9,color:#000
```
| Tier | Owns | Touches RED.* API? | Tested by |
|---|---|---|---|
| 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.
## generalFunctions — what it provides
The `nodes/generalFunctions` submodule is a plain-JS library every node depends on. Public exports (top-level `require('generalFunctions')`):
| 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.
## Output ports
Every EVOLV node emits on three ports:
```mermaid
flowchart LR
sc[specificClass]:::tier3
p0[(Port 0<br/>process data)]:::p0
p1[(Port 1<br/>InfluxDB line)]:::p1
p2[(Port 2<br/>registration / control)]:::p2
sc --> p0
sc --> p1
sc --> p2
p0 -.-> dn1[downstream Node-RED nodes<br/>dashboards, function nodes]
p1 -.-> influx[(InfluxDB)]
p2 -.-> parent[parent EVOLV node<br/>via child.register]
classDef tier3 fill:#50a8d9,color:#000
classDef p0 fill:#86bbdd
classDef p1 fill:#a9daee
classDef p2 fill:#dddddd
```
| Port | Carries | Format | Cardinality |
|---|---|---|---|
| **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. |
| **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. |
| **2** Registration / control | `child.register` upward on adapter init; control replies. | `{topic, payload: nodeRef}` | At init time + on demand. |
See [Telemetry](Telemetry) for the full Port-1 schema and InfluxDB conventions.
## Topic conventions
| Prefix | Direction | Used for |
|---|---|---|
| `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.
## 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
sequenceDiagram
participant childNc as Child nodeClass
participant parentReg as Parent commandRegistry
participant parentRouter as Parent ChildRouter
participant parentSc as Parent specificClass
childNc->>parentReg: msg{topic: child.register, softwareType, ref}
parentReg->>parentRouter: dispatch(child.register, ref)
parentRouter->>parentRouter: match softwareType
parentRouter->>parentSc: invoke registered handler
parentSc->>parentSc: store ref, wire emitter.on(...)
```
A child is anything the parent's `configure()` declares via `router.onRegister(<softwareType>, handler)`. Examples:
| Parent | Accepts children with softwareType |
|---|---|
| pumpingStation | `measurement`, `machine`, `machinegroup`, `pumpingstation` |
| machineGroupControl | `machine`, `measurement` |
| valveGroupControl | `valve`, `machine`, `machinegroup`, `pumpingstation`, `valvegroupcontrol` (last 4 as flow sources) |
| reactor | `measurement`, `reactor` |
| settler | `measurement`, `reactor`, `machine` |
| monster | `measurement` |
| diffuser | `measurement` |
| rotatingMachine | `measurement` |
| valve | `measurement` |
| dashboardAPI | any (used for Grafana provisioning) |
## Where business logic lives
```mermaid
flowchart TB
subgraph node["A node's src/ folder"]
sc["specificClass.js<br/>orchestration only"]
subgraph concerns["Concern subdirs (per-node)"]
c1[basin/ or curves/ or kinetics/<br/>physics / math]
c2[state/<br/>FSM transitions]
c3[dispatch/ or safety/<br/>action / guard logic]
c4[commands/<br/>topic → handler descriptors]
c5[io/<br/>output composition]
end
end
sc --> c1
sc --> c2
sc --> c3
sc --> c4
sc --> c5
```
specificClass should be **stitching only** — instantiate concern modules in `configure()`, call them in `tick()` or in router handlers. Concerns are individually testable.
## Reading order for newcomers
1. `.claude/refactor/CONTRACTS.md` — every API shape this wiki abstracts over.
2. `.claude/refactor/CONVENTIONS.md` — code style, file size, naming.
3. `.claude/refactor/MODULE_SPLIT.md` — concern layout per node.
4. One node's `wiki/Home.md` (pumpingStation is the most mature pilot — start there).
5. The corresponding `src/` folder, top-down: specificClass → concern modules.
## Related pages
- [Topology-Patterns](Topology-Patterns) — typical plant configurations
- [Topic-Conventions](Topic-Conventions) — naming and units
- [Telemetry](Telemetry) — Port-1 InfluxDB schema
- [Getting-Started](Getting-Started) — hands-on first run

@@ -1,3 +1,12 @@
> **⚠️ 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 # Claude API Token Permissions
For a safe **read + assist** role (Claude reads code, you approve and push), Claude needs: For a safe **read + assist** role (Claude reads code, you approve and push), Claude needs:

@@ -1,3 +1,12 @@
> **⚠️ 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 # EVOLV Configuration Model and Tagcodering
The intended long-term configuration authority for EVOLV is the database-backed `tagcodering` model. The intended long-term configuration authority for EVOLV is the database-backed `tagcodering` model.

@@ -1,3 +1,12 @@
> **⚠️ 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 # EVOLV Container Topology
This page translates the deployment blueprint into a practical container/service split. This page translates the deployment blueprint into a practical container/service split.

@@ -1,3 +1,12 @@
> **⚠️ 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 # EVOLV Deployment Blueprint
This page turns the architecture into a concrete deployment model. This page turns the architecture into a concrete deployment model.

@@ -1,3 +1,12 @@
> **⚠️ 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 # 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. This page translates the EVOLV architecture into deployment controls that can be checked during implementation, review, FAT/SAT, and audit preparation.

@@ -1,3 +1,12 @@
> **⚠️ 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 Platform Overview
EVOLV is a layered automation platform: EVOLV is a layered automation platform:

@@ -1,3 +1,12 @@
> **⚠️ 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 Security and Access Boundaries
EVOLV should expose central services, not field-edge systems, to external consumers. EVOLV should expose central services, not field-edge systems, to external consumers.

@@ -1,3 +1,12 @@
> **⚠️ 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 # EVOLV Security and Regulatory Mapping
This page maps major security and resilience requirement areas to the EVOLV architecture direction. This page maps major security and resilience requirement areas to the EVOLV architecture direction.

@@ -1,3 +1,12 @@
> **⚠️ 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 Telemetry and Smart Storage
EVOLV uses InfluxDB on multiple levels: EVOLV uses InfluxDB on multiple levels:

96
Archive-Source-SCHEMA.md Executable file

@@ -0,0 +1,96 @@
> **⚠️ 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

@@ -0,0 +1,64 @@
---
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

@@ -0,0 +1,286 @@
---
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

@@ -0,0 +1,53 @@
---
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).

@@ -0,0 +1,434 @@
---
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

@@ -0,0 +1,166 @@
---
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

@@ -0,0 +1,640 @@
---
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.

@@ -0,0 +1,462 @@
---
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
```

@@ -0,0 +1,28 @@
> **⚠️ 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`

@@ -0,0 +1,96 @@
---
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)

65
Archive-Source-index.md Executable file

@@ -0,0 +1,65 @@
---
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

@@ -0,0 +1,168 @@
> **⚠️ 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"}

19
Archive-Source-log.md Executable file

@@ -0,0 +1,19 @@
---
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

@@ -0,0 +1,211 @@
---
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`.

@@ -0,0 +1,255 @@
---
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).

64
Archive-Source-metrics.md Executable file

@@ -0,0 +1,64 @@
---
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 |

78
Archive-Source-overview.md Executable file

@@ -0,0 +1,78 @@
---
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 |

@@ -0,0 +1,54 @@
---
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)

@@ -0,0 +1,117 @@
---
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`).

@@ -0,0 +1,142 @@
---
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,9 +1,59 @@
# Archive — pre-refactor wiki pages # Archive — pre-refactor wiki pages
Pages kept for historical reference. **Do not update them.** Corrections go on the current page; if you find a meaningful inaccuracy in the archived page, leave it and add a note to the *current* page explaining what changed. 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.
Two batches of archived material:
## Live-wiki pages (originally on `EVOLV.wiki.git` before 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.
| Page | Era | Archived on | | Page | Era | Archived on |
|---|---|---| |---|---|---|
| _none yet_ | — | — | | [Architecture: Configuration Model & Tagcodering](Archive-Architecture-Configuration-Model-and-Tagcodering) | Pre-refactor planning doc | 2026-05-11 |
| [Architecture: Container Topology](Archive-Architecture-Container-Topology) | Pre-refactor Docker / container planning | 2026-05-11 |
| [Architecture: Deployment Blueprint](Archive-Architecture-Deployment-Blueprint) | Pre-refactor rollout plan | 2026-05-11 |
| [Architecture: Deployment Controls Checklist](Archive-Architecture-Deployment-Controls-Checklist) | Pre-refactor go/no-go checklist | 2026-05-11 |
| [Architecture: Platform Overview](Archive-Architecture-Platform-Overview) | Pre-refactor edge/site/central layering | 2026-05-11 |
| [Architecture: Security & Access Boundaries](Archive-Architecture-Security-and-Access-Boundaries) | Pre-refactor security model | 2026-05-11 |
| [Architecture: Security & Regulatory Mapping](Archive-Architecture-Security-and-Regulatory-Mapping) | Pre-refactor IEC 62443 mapping notes | 2026-05-11 |
| [Architecture: Telemetry & Smart Storage](Archive-Architecture-Telemetry-and-Smart-Storage) | Pre-refactor telemetry blueprint | 2026-05-11 |
| [AI-Assisted Coding](Archive-AI-assisted-coding.-) | Pre-refactor coding-with-AI usage note | 2026-05-11 |
Each archived page carries the standard banner at its top (see `.claude/refactor/WIKI_TEMPLATE.md` → Archive banner). ## Source-tree pages (originally under `EVOLV/wiki/` before 2026-05-11)
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 |
|---|---|---|
| [SCHEMA](Archive-Source-SCHEMA) | Pre-refactor wiki maintenance schema (Obsidian-style) | 2026-05-11 |
| [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 |
| [Metrics dashboard](Archive-Source-metrics) | Pre-refactor test counts (Apr-07 snapshot, 43 tests) | 2026-05-11 |
| [Project overview](Archive-Source-overview) | Pre-refactor node inventory ("needs review" for most nodes) | 2026-05-11 |
| [Architecture: Node architecture](Archive-Source-architecture-node-architecture) | Pre-refactor 3-tier diagram with old `_loadConfig` internals | 2026-05-11 |
| [Architecture: Platform overview](Archive-Source-architecture-platform-overview) | Pre-refactor edge/site/central vision | 2026-05-11 |
| [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 |
| [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 |
| [Concepts: Sources readme](Archive-Source-concepts-sources-readme) | Empty placeholder | 2026-05-11 |
| [Findings: Open issues 2026-03](Archive-Source-findings-open-issues-2026-03) | Issues 15 resolved by refactor | 2026-05-11 |
| [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
| For… | See |
|---|---|
| Top-level navigation | [Home](Home) |
| Code architecture (BaseDomain / 3-tier / generalFunctions) | [Architecture](Architecture) |
| Typical plant configurations | [Topology-Patterns](Topology-Patterns) |
| Topic naming + units + S88 colours | [Topic-Conventions](Topic-Conventions) |
| 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` |

127
Concept-ASM-Models.md Executable file

@@ -0,0 +1,127 @@
# Activated Sludge Models (ASM1, ASM2d, ASM3)
> **Used by**: `biological-process-engineer` agent, `reactor` node, `monster` node
> **Validation**: Verified against IWA publications, WaterTAP documentation, and peer-reviewed literature
## ASM1 — Activated Sludge Model No. 1
**Source**: Henze, M., Grady, C.P.L., Gujer, W., Marais, G.v.R., Matsuo, T. (1987). IAWPRC Task Group on Mathematical Modelling for Design and Operation of Biological Wastewater Treatment.
**Published**: IWA Scientific and Technical Report No. 1
### 13 Components (State Variables)
| Symbol | Description | Type |
|--------|-------------|------|
| S_I | Soluble inert organic matter | Soluble |
| S_S | Readily biodegradable substrate | Soluble |
| X_I | Particulate inert organic matter | Particulate |
| X_S | Slowly biodegradable substrate | Particulate |
| X_B,H | Active heterotrophic biomass | Particulate |
| X_B,A | Active autotrophic biomass | Particulate |
| X_P | Particulate products from biomass decay | Particulate |
| S_O | Dissolved oxygen | Soluble |
| S_NO | Nitrate and nitrite nitrogen | Soluble |
| S_NH | Ammonium nitrogen (NH₄⁺-N) | Soluble |
| S_ND | Soluble biodegradable organic nitrogen | Soluble |
| X_ND | Particulate biodegradable organic nitrogen | Particulate |
| S_ALK | Alkalinity (molar units) | Soluble |
### 8 Processes
1. **Aerobic growth of heterotrophs**: S_S + S_O + S_NH → X_B,H (Monod kinetics)
2. **Anoxic growth of heterotrophs**: S_S + S_NO + S_NH → X_B,H (denitrification)
3. **Aerobic growth of autotrophs**: S_NH + S_O → X_B,A + S_NO (nitrification)
4. **Decay of heterotrophs**: X_B,H → X_P + X_S (death-regeneration concept)
5. **Decay of autotrophs**: X_B,A → X_P + X_S
6. **Ammonification of soluble organic nitrogen**: S_ND → S_NH
7. **Hydrolysis of entrapped organics**: X_S → S_S
8. **Hydrolysis of entrapped organic nitrogen**: X_ND → S_ND
### Key Kinetic Parameters (default values at 20°C)
| Parameter | Symbol | Default | Unit | Description |
|-----------|--------|---------|------|-------------|
| Max. heterotrophic growth rate | μ_H | 6.0 | d⁻¹ | |
| Half-saturation (substrate) | K_S | 20.0 | g COD/m³ | |
| Half-saturation (oxygen, het.) | K_O,H | 0.2 | g O₂/m³ | |
| Half-saturation (nitrate) | K_NO | 0.5 | g NO₃-N/m³ | |
| Heterotrophic decay rate | b_H | 0.62 | d⁻¹ | |
| Max. autotrophic growth rate | μ_A | 0.8 | d⁻¹ | |
| Half-saturation (ammonia) | K_NH | 1.0 | g NH₃-N/m³ | |
| Half-saturation (oxygen, aut.) | K_O,A | 0.4 | g O₂/m³ | |
| Autotrophic decay rate | b_A | 0.05 | d⁻¹ | |
| Anoxic reduction factor | η_g | 0.8 | — | |
| Hydrolysis rate | k_h | 3.0 | g X_S/(g X_B,H · d) | |
| Yield (heterotrophic) | Y_H | 0.67 | g COD/g COD | |
| Yield (autotrophic) | Y_A | 0.24 | g COD/g N | |
| Fraction to X_P | f_P | 0.08 | — | |
### Temperature Correction
Arrhenius-type: k(T) = k(20) · θ^(T-20)
Common θ values:
- Heterotrophic growth: θ = 1.072
- Autotrophic growth: θ = 1.103 (nitrifiers are very temperature-sensitive)
- Decay: θ = 1.04
### Presentation Format
The model is presented in the **Petersen matrix** (also called Gujer matrix) format, where rows are processes and columns are components. Each cell contains the stoichiometric coefficient for how a process affects a component.
## ASM2d — Activated Sludge Model No. 2d
**Source**: Henze, M., Gujer, W., Mino, T., Matsuo, T., Wentzel, M.C., Marais, G.v.R., van Loosdrecht, M.C.M. (1999)
**Published**: IWA Scientific and Technical Report No. 3; also Water Science & Technology 39(1), 165-182
### Key Extensions over ASM1
- Adds **biological phosphorus removal** by phosphorus accumulating organisms (PAOs)
- Includes **denitrifying PAOs** (simultaneous P-removal and denitrification)
- 19 components, 21 processes
- Models storage of poly-hydroxy-alkanoates (PHA) and polyphosphate (poly-P) by PAOs
- Includes fermentation of readily biodegradable substrate
### Additional Components (beyond ASM1)
- S_F: Fermentable, readily biodegradable substrate
- S_A: Fermentation products (acetate)
- S_PO4: Soluble ortho-phosphate
- X_PAO: Phosphorus accumulating organisms
- X_PP: Poly-phosphate stored by PAOs
- X_PHA: Poly-hydroxy-alkanoates stored by PAOs
## ASM3 — Activated Sludge Model No. 3
**Source**: Gujer, W., Henze, M., Mino, T., van Loosdrecht, M.C.M. (1999); updated in Henze et al. (2001)
**Published**: IWA Scientific and Technical Report No. 9
### Key Differences from ASM1
- **Replaces death-regeneration** with endogenous respiration (conceptually simpler)
- **Introduces storage polymers** (X_STO) for heterotrophic biomass — substrate is first stored, then used for growth
- 13 state variables, 12 reactions
- More suitable for dynamic simulation and control applications
- Eliminates the problematic simultaneous storage/growth ambiguity in ASM1
### Storage-Based Metabolism
In ASM3, heterotrophs first store readily biodegradable substrate as internal storage products (X_STO), then grow on these stored products. This two-step process better reflects observed biological behavior.
## Choosing Between Models
| Criterion | ASM1 | ASM2d | ASM3 |
|-----------|------|-------|------|
| Carbon & nitrogen | Yes | Yes | Yes |
| Phosphorus removal | No | Yes | Via separate Bio-P module |
| Computational cost | Low | High | Medium |
| Calibration effort | Low | High | Medium |
| Best for | Carbon/N only WWTPs | Bio-P plants | Dynamic control |
## Authoritative References
1. Henze, M. et al. (1987). "Activated Sludge Model No. 1" — IAWPRC Scientific and Technical Report No. 1
2. Henze, M. et al. (1995). "Activated Sludge Model No. 2" — IAWQ Scientific and Technical Report No. 3
3. Henze, M. et al. (1999). "Activated Sludge Model No. 2d" — Water Sci. Technol. 39(1), 165-182
4. Gujer, W. et al. (1999). "Activated Sludge Model No. 3" — Water Sci. Technol. 39(1), 183-193
5. Henze, M. et al. (2000). "Activated Sludge Models ASM1, ASM2, ASM2d and ASM3" — IWA Publishing, ISBN 9781900222242
6. Jeppsson, U. (1996). "Modelling Aspects of Wastewater Treatment Processes" — Lund University PhD thesis (comprehensive ASM1 parameter listing)

122
Concept-InfluxDB-Schema-Design.md Executable file

@@ -0,0 +1,122 @@
# InfluxDB Time-Series Best Practices
> **Used by**: `telemetry-database` agent, `dashboardAPI` node
> **Validation**: Verified against InfluxDB official documentation (v1, v2, v3)
## Tag vs. Field Decision Framework
| Criterion | Use Tag | Use Field |
|-----------|---------|-----------|
| Queried in WHERE clause frequently | Yes | No |
| Used in GROUP BY | Yes | No |
| Low cardinality (< 100 distinct values) | Yes | Acceptable |
| High cardinality (IDs, timestamps, free text) | **Never** | Yes |
| Numeric measurement values | No | Yes |
| Needs aggregation (mean, sum, etc.) | No | Yes |
| Node/station/machine identifier | Yes | No |
| Actual sensor reading | No | Yes |
| Setpoint value | No | Yes |
| Quality flag | Depends* | Yes |
*Quality flags: If you have ≤5 quality levels (good/uncertain/bad), a tag is acceptable. If quality is a numeric score, use a field.
## EVOLV Tag/Field Convention
### Standard Tags (low cardinality, indexed)
```
locationId — Site identifier (e.g., "wwtp-brabant-01")
nodeType — Node type (e.g., "rotatingMachine", "reactor")
nodeName — Instance name (e.g., "pump-01", "reactor-A")
machineType — Equipment type (e.g., "pump", "blower", "valve")
stationId — Parent station identifier
measurementType — Sensor type (e.g., "flow", "pressure", "temperature")
```
### Standard Fields (not indexed, high cardinality)
```
value — Primary measurement value
setpoint — Control setpoint
quality — Data quality score (0.0-1.0)
state — Machine state (numeric code)
power — Power consumption (W)
efficiency — Current efficiency (0.0-1.0)
speed — Rotational speed (RPM or fraction)
position — Valve position (0.0-1.0)
```
## Cardinality Management
### What Is Cardinality?
Series cardinality = unique combinations of (measurement_name × tag_key_1 × tag_key_2 × ... × tag_key_n)
### Cardinality Limits
- **InfluxDB v1/v2 (TSM engine)**: High cardinality degrades query performance and increases memory usage. Keep below ~1M series per database.
- **InfluxDB v3**: Supports infinite series cardinality (new storage engine), but keeping cardinality low still improves query speed.
### Anti-Patterns (NEVER do these)
- Encoding timestamps in tag values
- Using UUIDs or session IDs as tags
- Free-text strings as tags
- Unbounded enum values as tags
- One measurement per sensor (use tags to differentiate instead)
### Good Patterns
- Use a single measurement name per data category
- Differentiate by tags, not by measurement name
- Keep tag value sets bounded and predictable
- Document all tag values in a schema registry
## Retention Policies
### Three-Tier Strategy
| Tier | Retention | Resolution | Purpose |
|------|-----------|------------|---------|
| Hot | 7-30 days | Full resolution (1s-10s) | Real-time dashboards, control loops |
| Warm | 90-365 days | Downsampled (1min-5min) | Trending, troubleshooting |
| Cold | 2-10 years | Heavily aggregated (1h-24h) | Compliance reporting, long-term trends |
### EVOLV Recommended Defaults
- Port 1 data at full resolution: 30 days
- 1-minute aggregates: 1 year
- 1-hour aggregates: 5 years (matches regulatory retention requirements)
## Continuous Queries / Tasks (Downsampling)
### InfluxDB v1: Continuous Queries
```sql
CREATE CONTINUOUS QUERY "downsample_1m" ON "evolv"
BEGIN
SELECT mean("value") AS "value", max("value") AS "max", min("value") AS "min"
INTO "rp_warm"."downsampled_1m"
FROM "telemetry"
GROUP BY time(1m), *
END
```
### InfluxDB v2: Tasks
```flux
option task = {name: "downsample_1m", every: 1m}
from(bucket: "telemetry")
|> range(start: -task.every)
|> aggregateWindow(every: 1m, fn: mean, createEmpty: false)
|> to(bucket: "telemetry-warm")
```
## Query Performance Tips
1. **Always filter by time range first** — time is the primary index
2. **Use tag filters in WHERE** — tags are indexed, fields are not
3. **Avoid regex on tag values** — use exact matches when possible
4. **Limit series scanned** — filter by specific nodeType/nodeName
5. **Use aggregation** — let the database aggregate rather than fetching raw points
6. **Batch writes** — write in batches of 5,000-10,000 points for optimal throughput
## Authoritative References
1. InfluxDB Documentation — "Schema Design and Data Layout" (https://docs.influxdata.com/influxdb/v1/concepts/schema_and_data_layout/)
2. InfluxDB Documentation — "Schema Design Recommendations and Best Practices" (v2/v3)
3. InfluxData Blog — "Time Series Data, Cardinality, and InfluxDB"
4. InfluxDB Documentation — "Resolve High Series Cardinality" (https://docs.influxdata.com/influxdb/v2/write-data/best-practices/resolve-high-cardinality/)
5. InfluxData (2023). "InfluxDB Best Practices" — Official technical guides

149
Concept-OT-Security-IEC62443.md Executable file

@@ -0,0 +1,149 @@
# OT Security Standards — IEC 62443 & NIST SP 800-82
> **Used by**: `ot-security-integration` agent
> **Validation**: Verified against IEC 62443 series, NIST SP 800-82, Dragos, and Rockwell Automation publications
## IEC 62443 Framework Overview
IEC 62443 "Industrial communication networks — IT security for networks and systems" is the primary international standard series for Industrial Automation and Control System (IACS) cybersecurity.
### Standard Structure
| Part | Title | Scope |
|------|-------|-------|
| 62443-1-x | General | Concepts, vocabulary, use cases |
| 62443-2-x | Policies & Procedures | Security management system, patch management |
| 62443-3-x | System | System security requirements, zones & conduits |
| 62443-4-x | Component | Secure development lifecycle, component requirements |
### Key Parts for EVOLV
- **62443-3-2**: Security risk assessment and system design
- **62443-3-3**: System security requirements and security levels
- **62443-4-2**: Technical security requirements for IACS components
## Zones and Conduits
### Security Zone
A **zone** is a logical or physical grouping of assets that share common security requirements. Assets within a zone have the same security level (SL) target.
### Conduit
A **conduit** is a logical or physical grouping of communication channels connecting two or more zones. Conduits require security controls appropriate for the traffic they carry.
### EVOLV Zone Architecture (typical WWTP deployment)
```
Zone 0: Enterprise IT Network (SL 1-2)
↕ [Conduit: Firewall/DMZ]
Zone 1: SCADA/Historian Network (SL 2-3)
↕ [Conduit: Data diode or filtered bridge]
Zone 2: Process Control Network (SL 3)
↕ [Conduit: Managed switch with ACLs]
Zone 3: Field Device Network (SL 2-3)
- PLCs, RTUs, I/O modules
- Node-RED edge runtime (EVOLV)
- Sensors and actuators
```
### Zone Design Rules
- A zone can contain sub-zones
- A conduit cannot contain sub-conduits
- A zone can have more than one conduit
- Every device must belong to exactly one zone
- Communication between zones must pass through a conduit
## Security Levels (SL)
| Level | Protection Against | Typical Measures |
|-------|-------------------|------------------|
| SL 0 | No specific requirements | — |
| SL 1 | Casual or coincidental violation | Basic authentication, logging |
| SL 2 | Intentional attack with low motivation, generic skills | Role-based access, encrypted communications |
| SL 3 | Intentional attack with moderate motivation, IACS-specific skills | Strong authentication, intrusion detection, hardened systems |
| SL 4 | Intentional attack with high motivation, IACS-specific skills, extended resources | Dedicated security team, continuous monitoring, zero-trust |
### SL Types
- **SL-T (Target)**: Required security level for the zone
- **SL-A (Achieved)**: Actual security level implemented
- **SL-C (Capability)**: Maximum security level a component can support
## NIST SP 800-82 — Guide to ICS Security
**Source**: NIST Special Publication 800-82 Revision 3 (2023). "Guide to Operational Technology (OT) Security"
### Key Recommendations
1. Develop and maintain an OT-specific security program
2. Segment OT networks from IT networks (defense in depth)
3. Apply least privilege access control
4. Monitor OT network traffic for anomalies
5. Maintain an accurate OT asset inventory
6. Implement secure remote access with MFA
7. Develop OT-specific incident response plans
8. Regularly assess and manage OT security risks
## OPC UA Security Model
### Authentication
- X.509 certificates for server and client authentication
- Username/password as fallback (less secure)
- Anonymous access (should be disabled in production)
### Encryption
- Security policies define algorithm suites:
- `None` — No security (testing only)
- `Basic128Rsa15` — Deprecated, avoid
- `Basic256` — Deprecated, avoid
- `Basic256Sha256` — Minimum recommended
- `Aes128_Sha256_RsaOaep` — Preferred
- `Aes256_Sha256_RsaPss` — Strongest
### Message Security Modes
- `None` — No signing or encryption
- `Sign` — Messages signed but not encrypted
- `SignAndEncrypt` — Full protection (recommended)
## Modbus Security Considerations
### Vulnerabilities (standard Modbus TCP)
- No authentication — any network client can read/write registers
- No encryption — all traffic is plaintext
- No integrity protection — commands can be modified in transit
- Predictable function codes — easy to craft malicious packets
### Mitigations
1. Network segmentation — isolate Modbus devices in dedicated VLANs
2. Firewall rules — whitelist only authorized master IP addresses
3. Application-layer filtering — deep packet inspection for Modbus function codes
4. Monitoring — detect unusual register access patterns
5. Modbus/TCP Security (TLS) — available in newer implementations (RFC 7878-based)
## EVOLV-Specific Security Considerations
### Node-RED Admin Endpoints
- `GET /<nodeName>/menu.js` — Serves configuration data to editor
- `GET /<nodeName>/configData.js` — Serves runtime config to editor
- **Risk**: Information disclosure if exposed beyond editor network
- **Mitigation**: Bind Node-RED to localhost or trusted network only
### msg.topic Input Validation
- All `msg.topic` handlers must validate topic format before processing
- Prevent topic injection: reject topics containing path separators, special characters
- Validate payload types and ranges before applying to control logic
### Dynamic Configuration
- Configuration loaded from files or received via MQTT
- Must validate schema, types, and value ranges before applying
- Reject configurations that would violate safety envelopes
### Control Message Safety
- Validate actuator commands against physical limits before sending
- Rate-limit control output changes (prevent rapid cycling)
- Log all control actions with timestamp, source, and reason
## Authoritative References
1. IEC 62443 series (2018-2024). "Industrial communication networks — IT security for networks and systems"
2. NIST SP 800-82 Rev 3 (2023). "Guide to Operational Technology (OT) Security"
3. Dragos Inc. — "Understanding ISA/IEC 62443: A Guide for OT Security Teams" (https://www.dragos.com/blog/isa-iec-62443-concepts)
4. ISA/IEC 62443-3-3 — "System Security Requirements and Security Levels"
5. OPC Foundation — "OPC UA Security Model" specification
6. Modbus Organization — "MODBUS/TCP Security" specification

168
Concept-PID-Control-Theory.md Executable file

@@ -0,0 +1,168 @@
# PID Control for Process Applications
> **Used by**: `mechanical-process-engineer` agent, `node-red-runtime` agent, `generalFunctions/src/pid/`
> **Validation**: Verified against Astrom & Hagglund (ISA, 2006) and MATLAB/Simulink documentation
## Continuous PID Controller
### Standard (ISA/Ideal) Form
```
u(t) = K_p · [e(t) + (1/T_i) · ∫e(τ)dτ + T_d · de(t)/dt]
```
Where:
- u(t) = controller output
- e(t) = error = setpoint - process variable (SP - PV)
- K_p = proportional gain
- T_i = integral time (seconds)
- T_d = derivative time (seconds)
### Parallel Form
```
u(t) = K_p · e(t) + K_i · ∫e(τ)dτ + K_d · de(t)/dt
```
Where K_i = K_p/T_i and K_d = K_p · T_d
## Discrete PID Implementation
### Positional Form (absolute output)
```
u[k] = K_p · e[k] + K_i · Δt · Σe[j] + K_d · (e[k] - e[k-1]) / Δt
```
- Computes the absolute output value each cycle
- Requires tracking of the integral sum
- Subject to integral windup
- Used when the controller output directly sets an actuator position
### Velocity Form (incremental output)
```
Δu[k] = K_p · (e[k] - e[k-1]) + K_i · Δt · e[k] + K_d · (e[k] - 2·e[k-1] + e[k-2]) / Δt
u[k] = u[k-1] + Δu[k]
```
- Computes the **change** in output each cycle
- Inherently bumpless on mode transfers (auto→manual→auto)
- Less prone to windup (but still needs anti-windup for output limits)
- Preferred for incremental actuators (VFDs, variable valves)
## Anti-Windup Strategies
Integral windup occurs when the controller output saturates (hits actuator limits) but the integral term continues to accumulate, causing large overshoot when the error changes sign.
### 1. Clamping (Conditional Integration)
Stop integrating when the output is saturated **and** the error has the same sign as the integral term:
```
if (u_raw > u_max || u_raw < u_min) && sign(e) == sign(integral):
freeze integral (do not accumulate)
else:
integral += e * Δt
```
- Simple to implement
- Effective for most process control applications
- The approach used in the EVOLV generalFunctions PID implementation
### 2. Back-Calculation
When the output saturates, feed back the difference between the saturated and unsaturated output to "unwind" the integrator:
```
integral += (K_i · e + K_b · (u_saturated - u_raw)) · Δt
```
Where K_b = 1/T_t (tracking time constant, typically T_t = √(T_i · T_d) or T_t = T_d).
- More sophisticated than clamping
- Better performance for systems with large disturbances
- Recommended by Astrom & Hagglund for demanding applications
### 3. Integrator Reset
Reset the integrator to a value that would produce the saturated output:
```
if u_raw > u_max:
integral = (u_max - K_p · e) / K_i
```
- Simple but can be aggressive
- May cause discontinuities
## Derivative Filtering
The derivative term amplifies high-frequency noise. Always filter it:
### First-Order Low-Pass Filter on D-Term
```
D_filtered[k] = α · D_filtered[k-1] + (1-α) · D_raw[k]
```
Where α = T_f / (T_f + Δt) and T_f = T_d / N (N typically 5-20, default 10).
### Derivative on PV (not Error)
To avoid derivative kick on setpoint changes:
```
D = -K_d · (PV[k] - PV[k-1]) / Δt (instead of using error)
```
This is the standard approach for process control.
## Cascade PID
Two nested loops where the outer (master) loop's output is the setpoint for the inner (slave) loop.
```
[SP_outer] → [PID_outer] → [SP_inner] → [PID_inner] → [Actuator] → [Process]
↑ ↑
[PV_outer] ←──── [Process] ←── [PV_inner]
```
### Design Rules
- Inner loop must be **5-10x faster** than outer loop
- Tune inner loop first (with outer loop in manual)
- Then tune outer loop
- Anti-windup on outer loop essential (its output is bounded by inner loop's SP limits)
### EVOLV Application
- Outer: Level controller → outputs flow setpoint
- Inner: Flow controller → outputs pump speed
- pumpingStation node coordinates this cascade
## Tuning Methods
### Ziegler-Nichols (Ultimate Gain Method)
1. Set I and D to zero, increase K_p until sustained oscillation
2. Record ultimate gain K_u and ultimate period T_u
3. Apply: K_p = 0.6·K_u, T_i = T_u/2, T_d = T_u/8
### Cohen-Coon (Process Reaction Curve)
1. Apply step change, record process reaction curve
2. Identify: gain K, dead time L, time constant τ
3. Apply formulas based on K, L, τ
### Lambda Tuning (IMC-based)
1. Identify process as FOPDT: K, L, τ
2. Choose closed-loop time constant λ (typically λ = max(3·L, τ))
3. K_p = τ/(K·λ), T_i = τ
- **Preferred for process control** — gives non-oscillatory response
- Directly specifies desired closed-loop speed
- Robust to model uncertainty when λ is chosen conservatively
## Authoritative References
1. Astrom, K.J. & Hagglund, T. (2006). "Advanced PID Control." ISA — The Instrumentation, Systems, and Automation Society.
2. Astrom, K.J. & Hagglund, T. (1995). "PID Controllers: Theory, Design, and Tuning." 2nd ed., ISA.
3. Seborg, D.E. et al. (2011). "Process Dynamics and Control." 3rd ed., Wiley. (Chapter on PID control)
4. MATLAB/Simulink documentation — "Anti-Windup Control Using PID Controller Block"
5. Smith, C.A. & Corripio, A.B. (2005). "Principles and Practices of Automatic Process Control." 3rd ed., Wiley.

154
Concept-Pump-Affinity-Laws.md Executable file

@@ -0,0 +1,154 @@
# Pump Affinity Laws & Curve Theory
> **Used by**: `mechanical-process-engineer` agent, `rotatingMachine` node, `pumpingStation` node
> **Validation**: Verified against Engineering Toolbox, Hydraulic Institute standards, and ScienceDirect
## Affinity Laws
The affinity laws describe how centrifugal pump performance scales with changes in rotational speed (N) or impeller diameter (D). They are derived from dimensional analysis under the assumption of geometric similarity (velocity triangles at the impeller remain geometrically similar).
### Speed Variation (constant diameter)
```
Q₂/Q₁ = N₂/N₁
H₂/H₁ = (N₂/N₁)²
P₂/P₁ = (N₂/N₁)³
```
### Diameter Variation (constant speed)
```
Q₂/Q₁ = D₂/D₁
H₂/H₁ = (D₂/D₁)²
P₂/P₁ = (D₂/D₁)³
```
### Combined Variation
```
Q₂/Q₁ = (N₂/N₁) · (D₂/D₁)
H₂/H₁ = (N₂/N₁)² · (D₂/D₁)²
P₂/P₁ = (N₂/N₁)³ · (D₂/D₁)³
```
### Practical Example (validated against Engineering Toolbox)
A pump at 1750 rpm delivering 100 gpm at 100 ft head using 5 bhp:
- At 3500 rpm: **200 gpm** flow, **400 ft** head, **40 bhp** power
- A 10% speed increase yields: +10% flow, +21% head, +33% power
### Accuracy Limitations
- Affinity laws are **approximate** — accuracy decreases with large speed changes (>±30%)
- Efficiency shifts slightly with speed change (not captured by basic affinity laws)
- Trimming impeller diameter >15-20% significantly reduces accuracy
- Laws assume no significant change in Reynolds number effects
## Pump Curve Types
### Q-H Curve (Flow vs. Head)
- Primary performance curve
- Head decreases as flow increases (for centrifugal pumps)
- Shape depends on specific speed (Ns): flat, steep, or drooping
- **Monotonicity**: Should be monotonically decreasing for stable operation. Non-monotonic (drooping) curves can cause instability in parallel operation.
### Q-P Curve (Flow vs. Power)
- Power consumption as function of flow
- Shape varies by pump type:
- Radial: power increases with flow (non-overloading possible at shutoff)
- Mixed flow: relatively flat
- Axial: power **decreases** with flow (overload risk at low flow)
### Q-η Curve (Flow vs. Efficiency)
- Efficiency peaks at Best Efficiency Point (BEP)
- Falls off on both sides of BEP
- Operating far from BEP causes excessive vibration, cavitation risk, and energy waste
## Best Efficiency Point (BEP)
The BEP is the operating point where the pump converts the maximum fraction of input power to useful hydraulic work.
### BEP Tracking Under VFD Control
When speed changes via VFD, the BEP shifts along a **parabolic path** in the Q-H plane:
```
H_BEP ∝ Q_BEP²
```
This is because both Q and H scale with speed, but H scales as the square of Q's scaling factor.
### Preferred Operating Region
- Continuous operation: 80% 110% of BEP flow
- Allowable range: 70% 120% of BEP flow
- Outside this range: increased bearing loads, seal wear, cavitation risk
## System Curve Theory
The system curve describes the head required by the piping system as a function of flow:
```
H_system = H_static + k · Q²
```
Where:
- H_static = static head (elevation difference + tank pressure difference)
- k = system resistance coefficient (Pa·s²/m⁶ in SI)
- Q = volumetric flow rate
### Duty Point
The **duty point** (operating point) is the intersection of the pump curve and system curve:
```
H_pump(Q) = H_system(Q)
```
This is solved numerically — find Q where pump curve equals system curve.
## Parallel Pump Operation
### Flow Summation Rule
For pumps in parallel at equal head:
```
Q_total = Q₁ + Q₂ + ... + Qₙ (at each head value)
```
The combined curve is constructed by **horizontally adding** individual pump curves.
### Key Considerations
- Each pump must overcome the same system head
- Adding a pump shifts the combined curve right, moving the operating point
- Diminishing returns: each additional pump adds less incremental flow
- Risk of back-flow through stopped pumps (check valves required)
- Unstable operation if pump curves have a drooping characteristic
## Series Pump Operation
For pumps in series at equal flow:
```
H_total = H₁ + H₂ + ... + Hₙ (at each flow value)
```
The combined curve is constructed by **vertically adding** individual pump curves.
## Specific Energy
The key energy KPI for pumping systems:
```
SE = P / Q [W / (m³/s) = J/m³]
```
More commonly expressed as:
```
SE = P / Q [kWh/m³] (with appropriate unit conversion)
```
Where:
- P = electrical power input (kW)
- Q = volumetric flow rate (m³/h)
- SE = P / Q · (1/1000) for kWh/m³ when P in W and Q in m³/s
### Wire-to-Water Efficiency
```
η_total = η_motor · η_VFD · η_pump = (ρ · g · Q · H) / P_electrical
```
## Authoritative References
1. Karassik, I.J. et al. "Pump Handbook" 4th ed. — McGraw-Hill (comprehensive pump engineering reference)
2. Europump/Hydraulic Institute (2001). "Pump Life Cycle Costs: A Guide to LCC Analysis for Pumping Systems"
3. Engineering Toolbox — "Affinity Laws for Pumps" (https://www.engineeringtoolbox.com/affinity-laws-d_408.html)
4. Hydraulic Institute Standards (HI 9.6.1 — Rotodynamic Pumps Guideline for NPSH Margin)
5. Gülich, J.F. (2014). "Centrifugal Pumps" 3rd ed. — Springer (theoretical foundation)

130
Concept-Settling-Models.md Executable file

@@ -0,0 +1,130 @@
# Sludge Settling & Clarifier Models
> **Used by**: `biological-process-engineer` agent, `settler` node
> **Validation**: Verified against Takacs et al. (1991), Vesilind (1968), and Burger-Diehl framework publications
## Vesilind Model — Zone Settling Velocity
**Source**: Vesilind, P.A. (1968). "Design of Prototype Thickeners from Batch Settling Tests." Water Sewage Works, 115, 302-307.
### Equation
```
v_s = v_0 · exp(-k · X)
```
Where:
- v_s = settling velocity (m/h)
- v_0 = maximum initial settling velocity (m/h)
- k = settling parameter (m³/kg or L/g)
- X = suspended solids concentration (kg/m³ or g/L)
### Typical Parameter Ranges for Municipal Wastewater
| Parameter | Typical Range | Unit | Notes |
|-----------|---------------|------|-------|
| v_0 | 4 12 | m/h | ~7.8 m/h is a commonly observed average |
| k | 0.3 0.8 | m³/kg | Correlates with SVI; higher SVI → higher k |
### SVI Correlation
The settling parameter k can be estimated from Sludge Volume Index:
- k ≈ 0.16 + 0.003 · SVI (for SVI in mL/g, k in m³/kg)
- Better correlations use SSVI (Stirred SVI) or DSVI (Diluted SVI)
### Limitations
- Only describes **zone settling** (hindered settling of a blanket)
- Does not capture compression settling at high concentrations
- Does not model the clarification zone (low-concentration region above blanket)
## Takacs Model — Double-Exponential Settling
**Source**: Takacs, I., Patry, G.G., Nolasco, D. (1991). "A dynamic model of the clarification-thickening process." Water Research, 25(10), 1263-1271.
### Equation
```
v_s = v_0 · (exp(-r_h · (X - X_min)) - exp(-r_p · (X - X_min)))
```
Where:
- v_s = settling velocity (m/h)
- v_0 = maximum Vesilind settling velocity (m/h)
- r_h = hindered settling parameter (m³/kg)
- r_p = flocculent settling parameter (m³/kg)
- X = suspended solids concentration (kg/m³)
- X_min = non-settleable fraction (kg/m³)
### Key Innovation
The double-exponential form captures **both** the clarification zone (low concentrations, dominated by the r_p term) and the thickening zone (high concentrations, dominated by the r_h term). This allows simulation of the complete solids profile from effluent to underflow.
### Typical Parameter Values
| Parameter | Typical Range | Default | Unit |
|-----------|---------------|---------|------|
| v_0 | 4 12 | 7.5 | m/h |
| r_h | 0.3 0.8 | 0.576 | m³/kg |
| r_p | 2.0 6.0 | 2.86 | m³/kg |
| X_min | 0 0.1 | 0.01 | kg/m³ |
### Sensitivity
- **r_p** is the most sensitive parameter — it governs effluent suspended solids
- Takacs et al. recommend finding r_p by simulation/calibration
- v_0 and r_h primarily affect the sludge blanket position and underflow concentration
### 1D Layer Model Implementation
The settler is divided into N horizontal layers (typically 10-30). For each layer:
1. Calculate settling velocity from local concentration
2. Apply solids flux theory (gravity flux + bulk flux)
3. Update concentration via mass balance
4. Handle feed layer, overflow, and underflow boundary conditions
## Burger-Diehl Framework — PDE-Based 1D Settler
**Source**: Burger, R., Diehl, S. and various co-authors (2011-present). Multiple publications developing the framework.
### Key Characteristics
- Based on rigorous **partial differential equation** theory (hyperbolic-elliptic PDE)
- Accounts for hindered settling, compression settling, and inlet dispersion
- Every implementation detail is consistent with PDE theory (unlike ad-hoc layer models)
- More realistic prediction of underflow sludge concentration
- Essential for accurate wet-weather modelling
### Advantages Over Takacs Layer Model
- Proper handling of compression settling (important at high MLSS)
- Mathematically rigorous — convergence guaranteed
- Better sludge blanket dynamics during storm events
- Can be extended with reactive terms (ASM1 biokinetics inside settler)
### When to Use Which Model
| Scenario | Recommended Model |
|----------|-------------------|
| Steady-state design | Vesilind + flux theory |
| Dynamic simulation (standard) | Takacs 1D layer model |
| Wet-weather / high-MLSS dynamics | Burger-Diehl PDE model |
| Quick estimation | Vesilind with SVI correlation |
## Flux Theory for Clarifier Design
The solids flux approach combines the gravity settling flux with the bulk (underflow) flux:
```
J_total = J_gravity + J_bulk = v_s(X) · X + Q_u/A · X
```
Where:
- J_total = total solids flux (kg/m²/h)
- v_s(X) = settling velocity at concentration X (from Vesilind or Takacs)
- Q_u = underflow rate (m³/h)
- A = clarifier surface area (m²)
The **limiting flux** determines the maximum solids loading rate — operating above this causes blanket rise and eventual washout.
## Authoritative References
1. Vesilind, P.A. (1968). "Design of Prototype Thickeners from Batch Settling Tests." Water Sewage Works, 115, 302-307.
2. Takacs, I., Patry, G.G., Nolasco, D. (1991). "A dynamic model of the clarification-thickening process." Water Res. 25(10), 1263-1271.
3. Burger, R., Diehl, S., Nopens, I. (2011). "A consistent modelling methodology for secondary settling tanks in wastewater treatment." Water Res. 45(6), 2247-2260.
4. Torfs, E. (2015). "Different settling regimes in secondary settling tanks." PhD thesis, Ghent University.
5. Daigger, G.T. (1995). "Development of refined clarifier operating diagrams using an updated settling characteristics database." Water Environment Research, 67(1), 95-100.

@@ -0,0 +1,157 @@
# Sensor Signal Conditioning & Data Quality
> **Used by**: `instrumentation-measurement` agent, `measurement` node
> **Validation**: Verified against IEC 61298, sensor manufacturer literature, and signal processing references
## Signal Conditioning Pipeline
```
Raw Signal → Scaling → Filtering → Outlier Rejection → Quality Flagging → Output
```
## Scaling: Engineering Unit Conversion
### 4-20 mA Standard
```
value = range_min + (I - 4) / (20 - 4) · (range_max - range_min)
```
Where I is the measured current in mA.
### Key Rules
- 0 mA = wire break (fault condition)
- < 4 mA = under-range or fault
- 4 mA = range minimum (0%)
- 20 mA = range maximum (100%)
- > 20 mA = over-range or fault
- NAMUR NE43 recommends 3.8 mA and 20.5 mA as fault thresholds
## Filtering Methods
### Moving Average
```
y[k] = (1/N) · Σ x[k-i] for i = 0 to N-1
```
- Simple, effective for white noise
- Introduces phase lag proportional to (N-1)/2 samples
- Good for steady-state signals, poor for fast transients
### Exponential Moving Average (EMA)
```
y[k] = α · x[k] + (1-α) · y[k-1]
```
Where α = 2/(N+1) or α = Δt/(τ + Δt) for time-constant-based tuning.
- Less memory than moving average
- Equivalent to first-order low-pass filter
- τ (time constant) sets the cutoff frequency: f_c = 1/(2π·τ)
### Savitzky-Golay Filter
- Fits a polynomial to a window of data points, uses the polynomial value as the filtered output
- Preserves higher-order moments (peaks, edges) better than moving average
- Configurable by window size and polynomial order
- Typical: window = 5-11 points, order = 2-3
## Outlier Detection
### Z-Score Method
```
z = |x - μ| / σ
outlier if z > threshold (typically 3.0)
```
- Assumes normal distribution
- Sensitive to the outliers themselves (they inflate σ)
### Modified Z-Score (MAD-based)
```
MAD = median(|x_i - median(x)|)
modified_z = 0.6745 · (x - median(x)) / MAD
outlier if |modified_z| > threshold (typically 3.5)
```
- Robust to outliers (uses median instead of mean)
- **Recommended for process measurements** where occasional spikes are common
- 0.6745 is the 75th percentile of the standard normal distribution
### IQR Method
```
Q1 = 25th percentile, Q3 = 75th percentile
IQR = Q3 - Q1
outlier if x < Q1 - 1.5·IQR or x > Q3 + 1.5·IQR
```
- Non-parametric, no distribution assumption
- Common in exploratory data analysis
- Less suitable for real-time streaming (needs window of data)
## NRMSE for Drift Detection
Normalized Root Mean Square Error compares a recent measurement window against a reference window to detect sensor drift.
### Calculation
```
RMSE = √(Σ(x_i - x_ref_i)² / N)
NRMSE = RMSE / (x_max - x_min) or RMSE / x_mean
```
### Thresholds (typical for process sensors)
| NRMSE Range | Quality | Action |
|-------------|---------|--------|
| 0 0.05 | Good | Normal operation |
| 0.05 0.15 | Uncertain | Flag for review, increase monitoring |
| 0.15 0.30 | Poor | Alarm, reduce weight in control loops |
| > 0.30 | Bad | Remove from control, maintenance required |
### Reference Window Selection
- Calibration data (gold standard)
- Post-maintenance baseline
- Rolling reference from a known-good period
- Multi-sensor cross-validation
## Sensor Accuracy Classes
### IEC 61298 Framework
IEC 61298 "Process measurement and control devices — General methods and procedures for evaluating performance" defines standardized test methods for evaluating sensor accuracy under reference and influence conditions.
### Key Performance Metrics
- **Accuracy**: Closeness of measured value to true value (includes systematic and random errors)
- **Repeatability**: Closeness of successive measurements under identical conditions
- **Hysteresis**: Maximum difference between upscale and downscale readings
- **Linearity**: Maximum deviation from a straight line between zero and span
- **Deadband**: Smallest change in input that produces a detectable output change
### Common Accuracy Specifications
| Sensor Type | Typical Accuracy | Response Time |
|-------------|-----------------|---------------|
| Pressure transmitter | ±0.04 0.1% FS | < 100 ms |
| Flow meter (electromagnetic) | ±0.2 0.5% of reading | 1-3 s |
| Temperature (RTD/Pt100) | ±0.1 0.3°C | 5-30 s (depends on housing) |
| Level (ultrasonic) | ±0.25% FS | 1-5 s |
| pH | ±0.02 0.1 pH | 10-60 s |
| Dissolved oxygen | ±1-2% of reading | 30-90 s (membrane) |
| Turbidity (nephelometric) | ±2% of reading | 5-15 s |
| Ammonia (ion-selective) | ±5-10% of reading | 60-180 s |
## Sensor States and Warmup
### State Machine
```
Maintenance → Warmup → Active → Cooldown → Maintenance
```
### Warmup Behavior
- **Duration**: Varies by sensor type (seconds for pressure, minutes for pH, hours for dissolved oxygen)
- **During warmup**: Measurements flagged as "uncertain" quality
- **Completion criterion**: Readings stabilize within defined tolerance for a minimum duration
- **EVOLV convention**: Warmup state prevents measurements from propagating to control loops
### Stabilization Detection
```
stable if std_dev(last_N_readings) < threshold for T_stable seconds
```
## Authoritative References
1. IEC 61298 series (2008). "Process measurement and control devices — General methods and procedures for evaluating performance"
2. IEC 61326-2-3. "Electrical equipment for measurement, control and laboratory use — EMC requirements — Part 2-3: Particular requirements — Transducers with integrated or remote signal conditioning"
3. NAMUR NE43 (2003). "Standardization of the Signal Level for the Failure Information of Digital Transmitters"
4. Bently Nevada / Baker Hughes. "Fundamentals of Rotating Machinery Diagnostics"
5. Oppenheim, A.V. & Willsky, A.S. (1997). "Signals & Systems" 2nd ed., Prentice Hall
6. Press, W.H. et al. (2007). "Numerical Recipes" 3rd ed., Chapter 14 (Savitzky-Golay filters)

@@ -0,0 +1,117 @@
# Dutch Wastewater Regulations & Compliance
> **Used by**: `commissioning-compliance` agent, `biological-process-engineer` agent
> **Validation**: Verified against EU Directive 91/271/EEC, Activiteitenbesluit milieubeheer, and Dutch water authority publications
## Regulatory Framework
### European Level
- **EU Urban Waste Water Treatment Directive 91/271/EEC** — Primary directive governing collection, treatment, and discharge of urban wastewater across all EU member states
- **Water Framework Directive 2000/60/EC** — Establishes river basin management and environmental quality standards
- **Revised UWWTD (2024)** — Updated directive with stricter nutrient limits and energy neutrality targets
### Dutch National Level
- **Waterwet** (Water Act) — National water management framework
- **Waterschapswet** (Water Authority Act) — Governance of regional water authorities
- **Activiteitenbesluit milieubeheer** — General rules for environmental activities including wastewater discharge
- **Besluit lozing afvalwater huishoudens** — Rules for domestic wastewater discharge
### Regional Level
- **Waterschap Brabantse Delta** — Regional water authority managing the target WWTP
- Operates under national framework with site-specific discharge permits (watervergunning)
## EU UWWTD Effluent Standards (Annex I)
### Table 1: Secondary Treatment Requirements
| Parameter | Concentration | Min. Reduction |
|-----------|--------------|----------------|
| BOD₅ (at 20°C, without nitrification) | 25 mg/L O₂ | 70-90% |
| COD | 125 mg/L O₂ | 75% |
| TSS (Total Suspended Solids) | 35 mg/L | 90% (>10,000 p.e.) |
| TSS (Total Suspended Solids) | 60 mg/L | 70% (2,000-10,000 p.e.) |
### Table 2: Nutrient Requirements for Sensitive Areas
| Parameter | Concentration | Min. Reduction |
|-----------|--------------|----------------|
| Total Phosphorus (10,000-100,000 p.e.) | 2 mg/L P | 80% |
| Total Phosphorus (>100,000 p.e.) | 1 mg/L P | 80% |
| Total Nitrogen (10,000-100,000 p.e.) | 15 mg/L N | 70-80% |
| Total Nitrogen (>100,000 p.e.) | 10 mg/L N | 70-80% |
*Note: The Netherlands designated its entire territory as a "sensitive area" under the UWWTD, meaning nutrient requirements (Table 2) apply to all significant WWTPs.*
### Dutch Practice (Often Stricter)
Dutch water authorities commonly set stricter limits than the EU minimum:
| Parameter | Typical Dutch Permit Limit | EU Minimum |
|-----------|---------------------------|------------|
| N-total | 5-10 mg/L | 10-15 mg/L |
| P-total | 0.3-1.0 mg/L | 1-2 mg/L |
| BOD₅ | 5-10 mg/L | 25 mg/L |
| COD | 50-100 mg/L | 125 mg/L |
| TSS | 10-20 mg/L | 35 mg/L |
| NH₄-N | 1-2 mg/L | Not specified in UWWTD |
*Note: Actual permit limits are site-specific. The values above represent common ranges for Dutch WWTPs.*
## Monitoring and Reporting Obligations
### Sampling Requirements (UWWTD Annex I, Table 3)
| Plant Size | Min. Annual Samples |
|------------|-------------------|
| 2,000-9,999 p.e. | 12 |
| 10,000-49,999 p.e. | 12 |
| ≥50,000 p.e. | 24 |
### Compliance Assessment
- Based on **annual averages** for most parameters
- A defined number of samples may fail while still meeting compliance (concentration limits)
- Percentage reduction assessed against influent loading
### Reporting Chain
1. WWTP operator monitors and reports to water authority
2. Water authority (Waterschap) reports to Province
3. Province reports to national government (IenW)
4. National government reports to European Commission
## Waterschap Brabantse Delta Context
### Service Area
- Province of Noord-Brabant (western part)
- Multiple WWTPs of varying sizes
- Mixed urban/agricultural/industrial catchment
### Key Challenges
- Agricultural runoff contributing nutrient loading
- Seasonal variations in flow and temperature
- Emerging contaminants (pharmaceuticals, microplastics)
- Energy efficiency targets (energy-neutral WWTP goal)
### EVOLV Relevance
- Process automation targets: optimizing energy use while maintaining effluent quality
- Real-time monitoring: continuous measurement of key parameters (NH₄, NO₃, PO₄, DO, TSS)
- Predictive control: using ASM models to anticipate process changes
- Reporting support: automated telemetry data for compliance reporting
## Key Compliance Parameters for EVOLV
| Parameter | Measurement Method | EVOLV Node | Typical Control Strategy |
|-----------|-------------------|------------|-------------------------|
| NH₄-N | Ion-selective electrode | measurement | Aeration control (DO setpoint cascade) |
| NO₃-N | UV absorption / ISE | measurement | Anoxic zone recirculation control |
| PO₄-P | Colorimetric / ISE | measurement | Chemical dosing or Bio-P optimization |
| DO | Amperometric / optical | measurement | Blower/diffuser control |
| TSS/MLSS | Optical (turbidity) | measurement | Sludge wasting control |
| Flow | Electromagnetic | measurement | Hydraulic load monitoring |
| Temperature | RTD/Pt100 | measurement | Process rate compensation |
## Authoritative References
1. Council Directive 91/271/EEC (1991). "Concerning urban waste water treatment" — Official Journal L 135/40
2. Directive (EU) 2024/3019. "Concerning urban wastewater treatment (recast)"
3. Activiteitenbesluit milieubeheer — Dutch Activities Decree (environmental management)
4. Waterschapswet — Dutch Water Authority Act
5. STOWA (Stichting Toegepast Onderzoek Waterbeheer) — Dutch Foundation for Applied Water Research (various publications on WWTP optimization)
6. Waterschap Brabantse Delta — Regional water management plans and discharge permits

@@ -0,0 +1,38 @@
---
title: BEP-Gravitation Optimality Proof
created: 2026-04-07
updated: 2026-04-07
status: proven
tags: [machineGroupControl, optimization, BEP, brute-force]
sources: [nodes/machineGroupControl/test/integration/distribution-power-table.integration.test.js]
---
# BEP-Gravitation vs Brute-Force Global Optimum
## Claim
The machineGroupControl BEP-Gravitation algorithm (with marginal-cost refinement) produces near-optimal flow distribution across a pump group.
## Method
Brute-force exhaustive search: 1000 steps per pump, all 2^n combinations, 0.05% flow tolerance. Station: 2x H05K-S03R + 1x C5-D03R-SHN1 @ ΔP=2000 mbar.
## Results
| Demand | Brute force | machineGroupControl | Gap |
|--------|------------|--------------------|----|
| 10% (71 m3/h) | 17.65 kW | 17.63 kW | -0.10% (MGC wins) |
| 25% (136 m3/h) | 34.33 kW | 34.33 kW | +0.01% |
| 50% (243 m3/h) | 61.62 kW | 61.62 kW | -0.00% |
| 75% (351 m3/h) | 96.01 kW | 96.10 kW | +0.08% |
| 90% (415 m3/h) | 122.17 kW | 122.26 kW | +0.07% |
Maximum deviation: **0.1%** from proven global optimum.
## Why the Refinement Matters
Before the marginal-cost refinement loop, the gap at 50% demand was **2.12%**. The BEP-Gravitation slope estimate pushed 14.6 m3/h to C5 (costing 5.0 kW) when the optimum was 6.5 m3/h (0.59 kW). The refinement loop corrects this by shifting flow from highest actual dP/dQ to lowest until no improvement is possible.
## Stability
Sweep 5-95% in 2% steps: 1 switch (rising), 1 switch (falling), same transition point. No hysteresis. See [[Pump Switching Stability]].
## Computational Cost
0.027-0.153ms median per optimization call (3 pumps, 6 combinations). Uses 0.015% of the 1000ms tick budget.

34
Finding-Curve-Non-Convexity.md Executable file

@@ -0,0 +1,34 @@
---
title: Pump Curve Non-Convexity
created: 2026-04-07
updated: 2026-04-07
status: proven
tags: [curves, interpolation, C5, non-convex]
sources: [nodes/generalFunctions/datasets/assetData/curves/hidrostal-C5-D03R-SHN1.json]
---
# Pump Curve Non-Convexity from Sparse Data
## Finding
The C5-D03R-SHN1 pump's power curve is non-convex after spline interpolation. The marginal cost (dP/dQ) shows a spike-then-valley pattern:
```
C5 dP/dQ across flow range @ ΔP=2000 mbar:
6.4 m3/h → 1,316,610 (high)
10.2 m3/h → 2,199,349 (spikes UP)
17.7 m3/h → 1,114,700 (dropping)
21.5 m3/h → 453,316 (valley — cheapest)
29.0 m3/h → 1,048,375 (rising again)
44.1 m3/h → 1,107,708 (high)
```
## Root Cause
The C5 curve has only **5 raw data points** per pressure level. The monotonic cubic spline (Fritsch-Carlson) creates a smooth curve through all 5 points, but with such sparse data it introduces non-convex regions that don't match the physical convexity of a real pump.
## Impact
- The equal-marginal-cost theorem (KKT conditions) does not apply — it requires convexity
- The BEP-Gravitation slope estimate at a single point can be misleading in non-convex regions
- The marginal-cost refinement loop fixes this by using actual power evaluations instead of slope assumptions
## Recommendation
Add more data points (15-20 per pressure level) to the C5 curve. This would make the spline track the real convex physics more closely, eliminating the non-convex artifacts.

42
Finding-NCog-Behavior.md Executable file

@@ -0,0 +1,42 @@
---
title: NCog Behavior and Limitations
created: 2026-04-07
updated: 2026-04-07
status: evolving
tags: [rotatingMachine, NCog, BEP, efficiency]
sources: [nodes/rotatingMachine/src/specificClass.js]
---
# NCog — Normalized Center of Gravity
## What It Is
NCog is a 0-1 value indicating where on its flow range a pump operates most efficiently. Computed per tick from the current pressure slice of the 3D pump curve.
```
BEP_flow = minFlow + (maxFlow - minFlow) * NCog
```
## How It's Computed
1. Pressure sensors update → `getMeasuredPressure()` computes differential
2. `fDimension` locks the 2D slice at current system pressure
3. `calcCog()` computes Q/P (specific flow) across the curve
4. Peak Q/P index → `NCog = (flowAtPeak - flowMin) / (flowMax - flowMin)`
## When NCog is Meaningful
NCog requires **differential pressure** (upstream + downstream). With only one pressure sensor, fDimension is the raw sensor value (too high), producing a monotonic Q/P curve and NCog = 0.
| Condition | NCog for H05K | NCog for C5 |
|-----------|--------------|-------------|
| ΔP = 400 mbar | 0.333 | 0.355 |
| ΔP = 1000 mbar | 0.000 | 0.000 |
| ΔP = 1500 mbar | 0.135 | 0.000 |
| ΔP = 2000 mbar | 0.351 | 0.000 |
## Why NCog = 0 Happens
For variable-speed centrifugal pumps, Q/P is monotonically decreasing when the affinity laws dominate (P ∝ Q³). At certain pressure levels, the spline interpolation preserves this monotonicity and the peak is always at index 0 (minimum flow).
## How the machineGroupControl Uses NCog
The BEP-Gravitation algorithm seeds each pump at its BEP flow, then redistributes using slope-based weights + marginal-cost refinement. Even when NCog = 0, the slope redistribution produces near-optimal results because it uses actual power evaluations.
> [!warning] Disproven: NCog as proportional weight
> Using NCog directly as a flow-distribution weight (`flow = NCog/totalNCog * Qd`) is wrong. It starves pumps with NCog = 0 and overloads high-NCog pumps. See `calcBestCombination` in machineGroupControl.

@@ -0,0 +1,34 @@
---
title: Pump Switching Stability
created: 2026-04-07
updated: 2026-04-07
status: proven
tags: [machineGroupControl, stability, switching]
sources: [nodes/machineGroupControl/test/integration/ncog-distribution.integration.test.js]
---
# Pump Switching Stability
## Concern
Frequent pump on/off cycling causes mechanical wear, water hammer, and process disturbance.
## Test Method
Sweep demand from 5% to 95% in 2% steps, count combination changes. Repeat in reverse to check for hysteresis.
## Results — Mixed Station (2x H05K + 1x C5)
Rising 5→95%: **1 switch** at 27% (H05K-1+C5 → all 3)
Falling 95→5%: **1 switch** at 25% (all 3 → H05K-1+C5)
Same transition zone, no hysteresis.
## Results — Equal Station (3x H05K)
Rising 5→95%: **2 switches**
- 19%: 1 pump → 2 pumps
- 37%: 2 pumps → 3 pumps
Clean monotonic transitions, no flickering.
## Why It's Stable
The marginal-cost refinement only adjusts flow distribution WITHIN a combination — it never changes which pumps are selected. Combination selection is driven by total power comparison, which changes smoothly with demand.

172
Getting-Started.md Executable file

@@ -0,0 +1,172 @@
# Getting Started
> **Reflects code as of `9ab9f6b` · regenerated `2026-05-11`**
How to clone, install, and run EVOLV locally; how to deploy the example flows; and where to go next.
## Prerequisites
| Tool | Version | Why |
|---|---|---|
| Node.js | ≥ 18 LTS | Node-RED 4 requires 18+ |
| npm | ≥ 9 | Comes with Node.js |
| git | ≥ 2.35 | Submodule support |
| Docker + compose v2 | optional | For the local Node-RED + InfluxDB stack |
| WSL2 (on Windows) | optional | Recommended for native docker performance |
## Clone and install
```bash
git clone --recurse-submodules https://gitea.wbd-rd.nl/RnD/EVOLV.git
cd EVOLV
npm install
```
If you cloned without `--recurse-submodules`, run:
```bash
git submodule update --init --recursive
```
There are 12 submodules — one per node (`generalFunctions` plus 11 active nodes). Each lives under `nodes/<name>/`.
## Verify the platform builds
```bash
npm run test:platform # expect 823 / 0
```
This runs the full unit test suite across all 12 submodules. ~3 minutes on a workstation (reactor's mathjs initialisation dominates).
## Option A — Run via Docker (recommended)
The repo ships a `docker-compose.yml` that brings up Node-RED + InfluxDB pre-loaded with the EVOLV nodes:
```bash
docker compose up -d
```
When healthy:
| Service | URL |
|---|---|
| Node-RED editor | http://localhost:1880 |
| FlowFuse dashboard (if widgets installed) | http://localhost:1880/dashboard |
| InfluxDB UI | http://localhost:8086 |
Watch the container logs while you click around:
```bash
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`.
## Option B — Run via local Node-RED
If you already have Node-RED installed in `~/.node-red`:
```bash
# in EVOLV/
ln -s "$PWD" ~/.node-red/nodes/EVOLV
```
Or add to your `~/.node-red/settings.js`:
```js
module.exports = {
// ...
nodesDir: ['/path/to/EVOLV/nodes'],
}
```
Then start Node-RED:
```bash
node-red
```
## Your first flow
Each node ships with example flows under `nodes/<name>/examples/`. The recommended starting point is **rotatingMachine — Basic Manual Control**:
```bash
# Copy the example into your Node-RED user dir
cp nodes/rotatingMachine/examples/01-Basic-Manual-Control.json ~/.node-red/
```
In the editor:
1. Menu → **Import** → select the file → **Import**.
2. Hit **Deploy**.
3. Open the dashboard at http://localhost:1880/dashboard.
4. Click the **startup** button. Watch the state machine progress: `idle → starting → warmingup → operational`.
5. Drag the demand slider. The flow + power predictions update in real time.
## What to read next
```mermaid
flowchart TB
start[You are here]:::neutral
arch[Architecture<br/>3-tier code structure]:::tier1
topo[Topology-Patterns<br/>typical plant configs]:::tier1
conv[Topic-Conventions<br/>naming + units]:::tier1
tele[Telemetry<br/>Port 0/1/2 + InfluxDB]:::tier1
node[Pick a node's wiki<br/>per-repo Home.md]:::tier3
start --> arch
start --> topo
arch --> node
topo --> node
node --> conv
node --> tele
classDef neutral fill:#dddddd
classDef tier1 fill:#a9daee,color:#000
classDef tier3 fill:#50a8d9,color:#000
```
| Path | Why |
|---|---|
| [Architecture](Architecture) | Internalise the 3-tier (entry → nodeClass → specificClass) pattern. |
| [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). |
| [Topic-Conventions](Topic-Conventions) | Reference for naming when you start wiring your own flows. |
| [Telemetry](Telemetry) | If you're plumbing InfluxDB or Grafana. |
## Quick command reference
```bash
# run all tests
npm run test:platform
# run one node's tests
cd nodes/rotatingMachine && node --test test/basic/*.test.js
# regenerate a node's wiki AUTOGEN blocks
cd nodes/rotatingMachine && npm run wiki:all
# rebuild docker stack
docker compose build && docker compose up -d
# update all submodules to their development tips
git submodule update --remote --recursive
# pack EVOLV as an npm tarball
npm pack
```
## Where to ask for help
| Channel | Use it for |
|---|---|
| Per-node wiki on Gitea | Operator-level questions for one node. |
| `.claude/refactor/OPEN_QUESTIONS.md` | Live decisions log — issues being worked on. |
| Gitea repo issues per submodule | File a bug against a specific node. |
| R&D team Slack / Teams | Anything urgent or strategic. |
## Related pages
- [Home](Home) — top-level navigation
- [Architecture](Architecture) — how a node is built
- [Topology-Patterns](Topology-Patterns) — plant configurations

132
Glossary.md Executable file

@@ -0,0 +1,132 @@
# Glossary
> **Reflects code as of `9ab9f6b` · regenerated `2026-05-11`**
Terms and abbreviations used across the EVOLV codebase, wikis, and dashboards.
## S88 (ISA-88 batch control)
```mermaid
flowchart TB
enterprise["Enterprise"]:::neutral
site["Site"]:::neutral
area["Area"]:::area
pc["Process Cell"]:::pc
unit["Unit"]:::unit
em["Equipment Module"]:::equip
cm["Control Module"]:::ctrl
enterprise --> site --> area --> pc --> unit --> em --> cm
classDef neutral fill:#dddddd
classDef area fill:#0f52a5,color:#fff
classDef pc fill:#0c99d9,color:#fff
classDef unit fill:#50a8d9,color:#000
classDef equip fill:#86bbdd,color:#000
classDef ctrl fill:#a9daee,color:#000
```
| Term | Meaning in EVOLV |
|---|---|
| **Area** | Plant section. *Reserved*, no node implements it yet. |
| **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`. |
| **Equipment Module (EM)** | A single piece of equipment. `rotatingMachine`, `valve`, `diffuser`. |
| **Control Module (CM)** | Single sensor / actuator. `measurement`. |
| **softwareType** | The node's S88 type — used by `ChildRouter` to match `child.register` calls. |
## EVOLV runtime concepts
| Term | Meaning |
|---|---|
| **BaseDomain** | Base class for every specificClass. Owns `measurements`, `router`, `emitter`, `logger`. |
| **BaseNodeAdapter** | Base class for every nodeClass. Bridges Node-RED ↔ specificClass via `commandRegistry`. |
| **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. |
| **ChildRouter** | Declarative matcher for `child.register` events. `router.onRegister(swType, handler)`. |
| **commandRegistry** | Inbound-msg dispatcher. Topic → handler descriptor map; resolves aliases and coerces units. |
| **UnitPolicy** | Per-node declaration of canonical (internal) and output units. |
| **MeasurementContainer** | Chainable measurement store. Keys: `<type>.<variant>.<position>.<childId>`. |
| **statusBadge** | Composes `node.status({fill,shape,text})` from a HealthStatus. |
| **HealthStatus** | `{level: 0..3, flags, message, source}` — standardised health summary. |
| **LatestWinsGate** | Mutex with supersede semantics — superseded calls resolve with `{superseded: true}`. |
| **outputUtils** | Single point for Port-0 delta compression + Port-1 line-protocol formatting. |
| **tick** | The 1 Hz update loop. Domain runs `tick()` for time-based concerns (integrators, FSM timers). |
| **getOutput()** | Domain method returning the current snapshot — fed to `outputUtils` for diff/format. |
| **getFlattenedOutput()** | Returns measurements with dot-flattened keys (4-segment: `type.variant.position.childId`). |
## Topic prefixes
(See [Topic-Conventions](Topic-Conventions) for the full table.)
| Prefix | Direction | Idempotent? |
|---|---|---|
| `set.` | inbound | yes |
| `cmd.` | inbound | no — has side-effects |
| `data.` | bidirectional | n/a |
| `evt.` | outbound | n/a |
| `child.` | inbound (parent receives) | yes — id-keyed |
## Wastewater treatment terms
| Term | Meaning |
|---|---|
| **WWTP** | Wastewater Treatment Plant. |
| **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, …). |
| **MLSS** | Mixed Liquor Suspended Solids — biomass concentration in the reactor. |
| **RAS** | Return Activated Sludge — settled sludge pumped back from settler to reactor. |
| **WAS** | Waste Activated Sludge — excess sludge removed from the system. |
| **TSS** | Total Suspended Solids. |
| **COD / BOD** | Chemical / Biological Oxygen Demand — organic load metrics. |
| **NH₄ / NO₃** | Ammonium / Nitrate — N species, key for nitrification/denitrification. |
| **DO** | Dissolved Oxygen, mg/L. Setpoint typically ~2 mg/L in aerated zones. |
| **K_La** | Volumetric mass-transfer coefficient (oxygen transfer rate / driving force). |
| **OTR** | Oxygen Transfer Rate — diffuser's output to the reactor. |
| **HRT / SRT** | Hydraulic / Sludge Retention Time. |
| **F/M ratio** | Food-to-microorganism ratio. |
| **Composite sample** | A sample built up over time, often proportional to flow — what `monster` simulates. |
## Pump / hydraulics terms
| Term | Meaning |
|---|---|
| **BEP** | Best Efficiency Point — operating point where the pump consumes the least energy per unit flow. |
| **NPSH** | Net Positive Suction Head — pressure margin to avoid cavitation. |
| **Affinity laws** | Q ∝ N, H ∝ N², P ∝ N³ — how flow/head/power scale with pump speed. |
| **Characteristic curve** | Q ↔ H (or Q ↔ P, Q ↔ η) graph supplied by the pump manufacturer. |
| **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. |
| **Setpoint** | The commanded value (e.g., flow setpoint = 12 m³/h). |
| **Demand** | The integrated requirement (e.g., the parent's "need this much flow now"). |
## Control / signal-processing terms
| Term | Meaning |
|---|---|
| **PID** | Proportional-Integral-Derivative controller. |
| **Anti-windup** | Prevents integral term from growing unboundedly when actuator saturates. |
| **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`). |
| **Smoothing** | Filtering noise (moving average, exponential, Savitzky-Golay). |
| **Outlier detection** | Identifying readings that fall outside expected variance. |
| **FSM** | Finite State Machine — discrete states + transitions. |
## Project terms
| Term | Meaning |
|---|---|
| **Tier 14** | Refactor phases (see [Home](Home) project status). |
| **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`. |
| **CONTRACTS.md** | API shapes at `.claude/refactor/CONTRACTS.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`. |
| **AUTOGEN block** | Sections of a wiki regenerated by `npm run wiki:all` — do not hand-edit between markers. |
## Related pages
- [Home](Home)
- [Architecture](Architecture)
- [Topic-Conventions](Topic-Conventions)
- [Topology-Patterns](Topology-Patterns)

120
Home.md

@@ -1,8 +1,9 @@
# EVOLV — Wastewater treatment plant automation # EVOLV — Wastewater Treatment Plant Automation
> **Reflects code as of `afc304b` · regenerated `2026-05-11` via `npm run wiki:home`** > **Reflects code as of `9ab9f6b` · regenerated `2026-05-11`**
> Source of truth: `nodes/<name>/src/specificClass.js` `configure()` declarations. Edges below were verified against `router.onRegister(...)` calls and emitter subscriptions.
EVOLV is a Node-RED node library for wastewater plant automation, developed by the R&D team at Waterschap Brabantse Delta. Nodes follow the ISA-88 (S88) batch control standard. The library exposes 11 active nodes spanning four S88 levels — from Process Cell down to Control Module — plus one utility node for dashboard integration. 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.
## Platform overview ## Platform overview
@@ -24,64 +25,85 @@ flowchart TB
diff[diffuser]:::equip diff[diffuser]:::equip
end end
subgraph CM["Control Module"] subgraph CM["Control Module"]
meas[measurement]:::ctrl meas["measurement<br/><i>registers with any process node</i>"]:::ctrl
end end
subgraph UT["Utility"] subgraph UT["Utility"]
dash[dashboardAPI]:::neutral dash["dashboardAPI<br/><i>any node → Grafana dashboard</i>"]:::util
end end
ps --> mgc
ps --> vgc ps -->|owns| mgc
mgc --> rm ps -.->|direct child, no group| rm
vgc --> v mgc -->|load-shares| rm
reactor --> diff vgc -->|positions| v
meas -.data.-> rm settler -->|return pump| rm
meas -.data.-> v
meas -.data.-> reactor reactor ==stateChange==> settler
meas -.data.-> settler diff -. OTR data .-> reactor
classDef pc fill:#0c99d9,color:#fff classDef pc fill:#0c99d9,color:#fff
classDef unit fill:#50a8d9,color:#000 classDef unit fill:#50a8d9,color:#000
classDef equip fill:#86bbdd,color:#000 classDef equip fill:#86bbdd,color:#000
classDef ctrl fill:#a9daee,color:#000 classDef ctrl fill:#a9daee,color:#000
classDef neutral fill:#dddddd,color:#000 classDef util fill:#dddddd,color:#000
``` ```
S88 colours: Process Cell `#0c99d9`, Unit `#50a8d9`, Equipment `#86bbdd`, Control Module `#a9daee`. Solid arrow = parent/child relationship. Dashed arrow = data flow (`measurement` feeds many node types). Source of truth: `.claude/rules/node-red-flow-layout.md` §14. **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)**.
## Live nodes ## Live nodes
| S88 | Node | One-liner | Wiki | | S88 level | Node | One-liner | Per-node wiki |
|---|---|---|---| |---|---|---|---|
| Process Cell | **pumpingStation** | Manages a wet-well basin, hands demand to one or more group controllers. | [](https://gitea.wbd-rd.nl/RnD/pumpingStation/wiki/Home) | | 🟦 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) |
| Unit | **machineGroupControl** | Load-sharing across a group of rotatingMachines. | [](https://gitea.wbd-rd.nl/RnD/machineGroupControl/wiki/Home) | | 🔷 Unit | **machineGroupControl** | Load-sharing across a group of `rotatingMachine` children. | [Home ](https://gitea.wbd-rd.nl/RnD/machineGroupControl/wiki/Home) |
| Unit | **valveGroupControl** | Coordinated valve control across a group of valves. | [](https://gitea.wbd-rd.nl/RnD/valveGroupControl/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 | **reactor** | Bioreactor — couples diffuser + measurements + ASM kinetics. | [](https://gitea.wbd-rd.nl/RnD/reactor/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 | **settler** | Settler / clarifier modelling. | [](https://gitea.wbd-rd.nl/RnD/settler/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 | **monster** | Composite-sample sensor surrogate / multi-parameter monitor. | [](https://gitea.wbd-rd.nl/RnD/monster/wiki/Home) | | 🔷 Unit | **monster** | Composite-sample sensor surrogate / proportional sampling program. | [Home ](https://gitea.wbd-rd.nl/RnD/monster/wiki/Home) |
| Equipment | **rotatingMachine** | Single pump / compressor — curves, state machine, prediction. | [](https://gitea.wbd-rd.nl/RnD/rotatingMachine/wiki/Home) | | 🟦 Equipment | **rotatingMachine** | Single pump / compressor — characteristic curves, prediction, FSM. | [Home ](https://gitea.wbd-rd.nl/RnD/rotatingMachine/wiki/Home) |
| Equipment | **valve** | Single valve actuator with FSM. | [](https://gitea.wbd-rd.nl/RnD/valve/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 | **diffuser** | Aeration diffuser, gas-side modelling. | [](https://gitea.wbd-rd.nl/RnD/diffuser/wiki/Home) | | 🟦 Equipment | **diffuser** | Aeration diffuser; gas-side modelling, OTR emission to reactor. | [Home ](https://gitea.wbd-rd.nl/RnD/diffuser/wiki/Home) |
| Control Module | **measurement** | Sensor signal-conditioning, scaling, calibration. | [](https://gitea.wbd-rd.nl/RnD/measurement/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) |
| Utility | **dashboardAPI** | Bridge between EVOLV nodes and Grafana dashboard upserts. | [](https://gitea.wbd-rd.nl/RnD/dashboardAPI/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) |
| — | **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) |
Plus the shared library `generalFunctions` — not a Node-RED node itself; provides `BaseDomain`, `BaseNodeAdapter`, `ChildRouter`, `UnitPolicy`, `MeasurementContainer`, command registry, logger, and config manager. ## Start here
## Standards & conventions | You want to… | Read |
|---|---|
| Stand up a local dev environment + run an example flow | [Getting-Started](Getting-Started) |
| Understand the codebase layout, BaseDomain/adapter pattern, output ports | [Architecture](Architecture) |
| 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) |
| Understand what Port 0 / Port 1 / Port 2 carry, InfluxDB layout | [Telemetry](Telemetry) |
| Decode S88 / EVOLV jargon | [Glossary](Glossary) |
| Document | What it covers | Where | ## Domain concepts
|---|---|---|
| Node architecture (3-tier) | entry → nodeClass → specificClass | `.claude/rules/node-architecture.md` |
| Flow layout (Node-RED tabs) | Tab boundaries, lanes, S88 colours, link channels | `.claude/rules/node-red-flow-layout.md` |
| Telemetry (Port 0/1/2) | InfluxDB line protocol, cardinality, FlowFuse compatibility | `.claude/rules/telemetry.md` |
| generalFunctions stability | What's safe to change in the shared lib | `.claude/rules/general-functions.md` |
| repo-mem MCP usage | When to use `repo_search` / `repo_record_fix` instead of grep | `.claude/rules/repo-mem.md` |
| Refactor goals + tiers | Why the refactor exists, sequencing | `.claude/refactor/README.md` |
| Code conventions | Style, file size, comments, naming, imports, tests | `.claude/refactor/CONVENTIONS.md` |
| Contracts | `BaseNodeAdapter`, `BaseDomain`, commands registry, child router, unit policy, status badge, output ports | `.claude/refactor/CONTRACTS.md` |
| Module split | Per-node `src/` layout for the 4 core nodes + generic template | `.claude/refactor/MODULE_SPLIT.md` |
| Wiki per-node template | The 14-section page shape | `.claude/refactor/WIKI_TEMPLATE.md` |
| Wiki home + archive | This page's template | `.claude/refactor/WIKI_HOME_TEMPLATE.md` |
## Refactor status Evergreen technical references (not affected by refactors):
| Page | Topic |
|---|---|
| [ASM models](concepts/asm-models) | Activated Sludge Models — biological process kinetics |
| [PID control theory](concepts/pid-control-theory) | Loop tuning, anti-windup, controller forms |
| [Pump affinity laws](concepts/pump-affinity-laws) | Speed/flow/head/power scaling |
| [Settling models](concepts/settling-models) | Takács / Vesilind / discrete settling |
| [Signal processing — sensors](concepts/signal-processing-sensors) | Smoothing, outlier rejection |
| [InfluxDB schema design](concepts/influxdb-schema-design) | Cardinality, tags vs fields |
| [Wastewater compliance NL](concepts/wastewater-compliance-nl) | Dutch regulatory context |
| [OT security — IEC 62443](concepts/ot-security-iec62443) | OT cybersecurity baseline |
## Operations findings
Algorithm-level proofs and behavioural notes that are still valid:
| Page | Topic |
|---|---|
| [BEP gravitation proof](findings/bep-gravitation-proof) | Best-efficiency-point convergence |
| [Curve non-convexity](findings/curve-non-convexity) | When pump curves break local optima |
| [NCog behaviour](findings/ncog-behavior) | NCog control metric notes |
| [Pump switching stability](findings/pump-switching-stability) | Hysteresis design for multi-pump groups |
## Project status
| Tier | What | Status | | Tier | What | Status |
|---|---|---| |---|---|---|
@@ -90,12 +112,14 @@ Plus the shared library `generalFunctions` — not a Node-RED node itself; provi
| 3 | Convert measurement, MGC, rotatingMachine | ✅ done | | 3 | Convert measurement, MGC, rotatingMachine | ✅ done |
| 4 | Convert valve, VGC, reactor, settler, monster, diffuser, dashboardAPI | ✅ done | | 4 | Convert valve, VGC, reactor, settler, monster, diffuser, dashboardAPI | ✅ done |
| 5 | Canonical topic names + alias deprecation map | ✅ done | | 5 | Canonical topic names + alias deprecation map | ✅ done |
| 6 | Promote `development``main` | ✅ done | | 6 | Promote `development``main` | ⏳ pending Docker E2E + human review |
| 8.5 | Remove deprecated paths in `generalFunctions` | ✅ done | | 8.5 | Remove deprecated paths in `generalFunctions` | ✅ done |
| 9 | Wiki cleanup — visual-first template + per-node Home pages | 🟡 partial (pumpingStation + dashboardAPI landed; 9 nodes to go) | | 9 | Wiki refactor — visual-first per-node + master pages | ✅ landed 2026-05-11 |
| 10 | Test-suite refactor across all nodes | in progress | | 10 | Test-suite refactor across all nodes | 🟡 in progress |
| — | pumpingStation Docker E2E (P2.14) | ⏳ pending | | — | pumpingStation Docker E2E (P2.14) | ⏳ pending |
823 platform tests pass · 0 failures · 12 submodules + parent on `development`.
## Archive ## Archive
Pre-refactor pages live under `Archive/`. See [Archive index](Archive). Pre-refactor planning pages have been moved to the [Archive](Archive). The current Home and supporting pages are the canonical references.

@@ -0,0 +1,47 @@
# FlowFuse Dashboard Layout Notes (EVOLV Reference)
Primary sources:
- https://dashboard.flowfuse.com/
- https://dashboard.flowfuse.com/nodes/widgets/ui-chart.html
## Compact Screen Guidelines
- Use a 12-column page grid and place charts in 4-column blocks for 3-up rows.
- Disable legends for single-series charts to reduce visual noise.
- Prefer concise text widgets under charts for state/timing/snapshot summaries.
- Use compact theme spacing:
- lower page padding
- lower group gap
- lower widget gap
## Time Window Guidelines
- For live demos, default chart history to 10-15 minutes.
- Keep axis labels short and unit-specific.
- Use one chart per KPI unless comparison is intentionally needed.
## Message Hygiene For Widgets
- Chart widgets: send minimal `{ topic, payload, timestamp }`.
- Text widgets: send plain string in `msg.payload`.
- Separate chart and text outputs by Function-node output index.
## Gauge Sizing in Groups
- **Tank gauge**: `width: 2, height: 4` — tall vertical fill indicator.
- **3/4 arc gauge**: `width: 2, height: 3` — fits beside tank in same group row.
- **Status text**: `width: 4, height: 1` — full group width, below gauges.
- In a 4-column group, two `width: 2` gauges sit side by side, text below spans full width.
- Set `order` on widgets: tank=2, arc=3, text=1 (text first = top, gauges below; or tank=1, arc=2, text=3 for gauges on top).
## Group Height Auto-sizing
- Set `height: "1"` on groups to auto-grow with content.
- A group with a `height: 4` tank + `height: 1` text will auto-expand to ~5 rows.
## References
- FlowFuse Dashboard docs root: https://dashboard.flowfuse.com/
- FlowFuse `ui-chart` docs: https://dashboard.flowfuse.com/nodes/widgets/ui-chart.html
- FlowFuse Widget catalog: [flowfuse-widgets-catalog.md](flowfuse-widgets-catalog.md)
- FlowFuse Config nodes: [flowfuse-ui-config-manual.md](flowfuse-ui-config-manual.md)

@@ -0,0 +1,62 @@
# FlowFuse `ui-button` Manual (EVOLV Reference)
Source: https://dashboard.flowfuse.com/nodes/widgets/ui-button.html
## Purpose
Clickable button that sends a message on user interaction.
## Properties
| Property | Type | Dynamic | Notes |
|----------|------|---------|-------|
| `group` | ref | No | Parent ui-group |
| `width` | int | No | Columns |
| `height` | int | No | Row units |
| `label` | string | Yes | Button text |
| `icon` | string | Yes | Material Design icon name (no `mdi-` prefix) |
| `iconPosition` | string | Yes | `"left"` or `"right"` |
| `buttonColor` | string | Yes | Background color |
| `textColor` | string | Yes | Label color (auto-calculated if omitted) |
| `iconColor` | string | Yes | Icon color (matches text if omitted) |
| `tooltip` | string | No | Hover tooltip |
| `order` | int | No | Position in group |
| `emulateClick` | bool | No | Trigger click on any received msg |
## Events
| Event | Enabled By | Output |
|-------|-----------|--------|
| Click | `onclick: true` | `msg.payload` = configured value |
| Pointer Down | `onpointerdown: true` | `msg.payload` + `msg._event` with timestamp |
| Pointer Up | `onpointerup: true` | `msg.payload` + `msg._event` with timestamp |
Hold duration = `pointerup._event.timestamp - pointerdown._event.timestamp`.
## Input
`msg.payload` — sets button payload value. With `emulateClick: true`, any input msg triggers a click.
`msg.enabled``true` / `false` to enable/disable the button.
## Dynamic Properties (`msg.ui_update`)
```js
msg.ui_update = {
label: "Stop",
icon: "stop",
iconPosition: "left",
buttonColor: "#f44336",
textColor: "#ffffff",
iconColor: "#ffffff",
class: "my-btn-class"
};
```
## EVOLV Key Rules
1. Use buttons for operator actions (start/stop pump, acknowledge alarm).
2. Set `emulateClick: false` (default) — don't auto-trigger on incoming messages.
3. For toggle buttons, update label/color dynamically via `msg.ui_update` from downstream logic.
4. Pair with confirmation dialog (ui-notification or ui-template) for destructive actions.
5. Standard sizing: `width: 2, height: 1` for inline; `width: 4, height: 1` for full-width in 4-col group.

@@ -0,0 +1,121 @@
# FlowFuse `ui-chart` Manual (EVOLV Reference)
Source: https://dashboard.flowfuse.com/nodes/widgets/ui-chart.html
## Chart Types
| Type | X-Axis Options | Notes |
|------|---------------|-------|
| Line | timescale, linear, categorical | Interpolation: linear, step, bezier, cubic, cubic-mono |
| Bar | categorical | Grouped (side-by-side) or stacked |
| Scatter | timescale, linear, categorical | Configurable point shape & radius |
| Pie / Doughnut | radial | Nested series for multi-layer |
| Histogram | auto-binned | Numerical bins or categorical counting |
## Properties
| Property | Type | Dynamic | Notes |
|----------|------|---------|-------|
| `group` | ref | No | Parent ui-group |
| `width` | int | No | Columns |
| `height` | int | No | Row units |
| `label` | string | No | Chart title |
| `chartType` | string | No | `"line"`, `"bar"`, `"scatter"`, `"pie"`, `"histogram"` |
| `showLegend` | bool | No | Show series legend |
| `action` | string | Yes | `"append"` or `"replace"` |
| `pointShape` | string | No | Point marker shape (scatter/line) |
| `xAxisLimit` | int | No | Max data points to keep |
| `textColor` | color | No | Override text color |
| `gridColor` | color | No | Override grid lines |
| `xAxisType` | string | No | `"time"`, `"linear"`, `"category"` |
## Series Configuration
| Source | How It Works |
|--------|-------------|
| `msg.topic` | Default — each unique topic creates a series |
| `key` | Uses a named property from data object |
| `JSON` | Array of keys for multi-series from single msg |
| (blank) | Auto-generates timestamp for x |
## Input Format
### Simple (recommended for EVOLV)
```js
{ topic: "PS West Level", payload: 2.34, timestamp: Date.now() }
```
### Object with x/y mapping
```js
{ payload: { time: 1700000000000, value: 2.34 } }
// Configure x → "time", y → "value"
```
### Nested access
X/Y keys support dot notation: `"nested.value"``payload.nested.value`
## Dynamic Properties
| Property | Path | Notes |
|----------|------|-------|
| Action | `msg.action` | `"append"` or `"replace"` |
| Chart Options | `msg.ui_update.chartOptions` | eCharts config merge |
| CSS Class | `msg.class` | Add custom class |
## eCharts Customization (`msg.ui_update.chartOptions`)
Deep-merges with existing config. Accumulates across messages.
```js
msg.ui_update = {
chartOptions: {
yAxis: { position: "right", name: "Level (m)" },
grid: { top: 60, right: 40 },
title: { text: "Basin Levels", textStyle: { fontSize: 14 } }
}
};
```
**Series colors** — must provide all series configs together:
```js
msg.ui_update = {
chartOptions: {
series: [
{ name: "PS West", type: "line", lineStyle: { color: "#2196f3" }, itemStyle: { color: "#2196f3" } },
{ name: "PS North", type: "line", lineStyle: { color: "#ff9800" }, itemStyle: { color: "#ff9800" } }
]
}
};
```
## Data Actions
| Action | How | Effect |
|--------|-----|--------|
| Append | `msg.action = "append"` (default) | Add points to existing data |
| Replace | `msg.action = "replace"` | Clear then add |
| Clear | `msg.payload = []` | Remove all data |
## Time Formatting Tokens
`{HH}:{mm}:{ss}``12:00:00`
`{yyyy}-{M}-{d}``2020-1-1`
Tokens: `{yyyy}`, `{MM}`, `{dd}`, `{HH}`, `{H}`, `{hh}`, `{h}`, `{mm}`, `{m}`, `{ss}`, `{s}`, `{eeee}` (day name)
## EVOLV Key Rules
1. Send minimal `{ topic, payload, timestamp }` — chart auto-uses topic for series.
2. Set `category: "topic"`, `categoryType: "msg"` in node config — blank category causes editor validation errors.
3. For timeseries: leave x-key blank → auto-timestamp. Supply ms timestamps if custom.
4. For multi-station overlay charts: each station sends with a different `msg.topic`.
5. Use `msg.action = "append"` (default) for streaming data; `"replace"` for snapshot updates.
6. Keep one topic per chart for simple KPIs; use multi-topic for comparison views.
7. Prefer explicit output-slot routing: `node.send([msgForChart, msgForText, ...]); return null;`
## Common Failure Modes
- Payload is not numeric (string/object without y-key config)
- Function node returns to wrong output index
- Topic churn creates unexpected series fragmentation
- Chart has stale data policy mismatch (`append` vs `replace`)
- Blank `category` field causes red node on deploy

@@ -0,0 +1,103 @@
# FlowFuse Config Nodes Manual (EVOLV Reference)
Sources:
- https://dashboard.flowfuse.com/nodes/config/ui-base.html
- https://dashboard.flowfuse.com/nodes/config/ui-page.html
- https://dashboard.flowfuse.com/nodes/config/ui-group.html
- https://dashboard.flowfuse.com/nodes/config/ui-theme.html
---
## `ui-base` — Dashboard Root
| Property | Type | Notes |
|----------|------|-------|
| `path` | string | URL path after host, e.g. `/dashboard` |
| `appIcon` | string | URL to square icon (192512px) for PWA |
| `includePagePath` | bool | Show page paths in side nav |
| `navigationStyle` | string | `"default"`, `"fixed"`, `"icon"`, `"temporary"`, `"none"` |
| `headerStyle` | string | `"default"` (scrolls), `"fixed"` (sticky), `"hidden"` |
| `headerContent` | string | `"page"`, `"dashboard"`, `"both"`, `"none"` |
---
## `ui-page` — Dashboard Page
| Property | Type | Notes |
|----------|------|-------|
| `name` | string | Displayed in nav and header |
| `ui` | ref | Parent ui-base |
| `path` | string | URL path segment, e.g. `/influent` |
| `icon` | string | Material Design icon (no `mdi-` prefix) |
| `theme` | ref | ui-theme reference |
| `layout` | string | `"grid"`, `"fixed"`, `"notebook"`, `"tabs"` |
| `order` | int | Position in navigation |
| `breakpoints` | array | See breakpoints table |
### Layout Types
| Layout | Description |
|--------|-------------|
| `grid` | Responsive grid, widgets flow into columns |
| `fixed` | Absolute positioned, no responsive reflow |
| `notebook` | Single-column stacked groups |
| `tabs` | Each group becomes a tab |
### Default Breakpoints (grid/notebook/tabs)
| Name | Min Width | Columns |
|------|-----------|---------|
| Mobile | 0 px | 3 |
| Medium | 576 px | 6 |
| Tablet | 768 px | 9 |
| Desktop | 1024 px | 12 |
---
## `ui-group` — Widget Container
| Property | Type | Notes |
|----------|------|-------|
| `name` | string | Group title (shown if `showTitle: true`) |
| `page` | ref | Parent ui-page |
| `width` | string/int | Column span (e.g. `"4"` = 4 of 12 columns) |
| `height` | string/int | Minimum row height (`"1"` = auto-grow) |
| `order` | int | Render order on page |
| `showTitle` | bool | Show name as header |
| `className` | string | Custom CSS class |
| `groupType` | string | `"default"` (visible) or `"dialog"` (triggered) |
### Sizing Rules
- Group `width` sets column span out of page's total columns (default 12).
- Group `height` is a **minimum** — group grows to fit content.
- Widget `width` within a group is relative to the group's width.
- Widget `height` is in row units (1 unit = theme's Row Height setting).
---
## `ui-theme` — Appearance
| Property | Type | Default | Notes |
|----------|------|---------|-------|
| `colors.surface` | color | — | Header & nav background |
| `colors.primary` | color | — | Buttons, sliders, focus rings |
| `colors.bgPage` | color | — | Page background |
| `colors.groupBg` | color | — | Group background |
| `colors.groupOutline` | color | — | Group border color |
| `sizes.rowHeight` | string | `"default"` | `"default"` (48px), `"comfortable"` (36px), `"compact"` (32px) |
| `sizes.pagePadding` | string | `"12px"` | CSS shorthand (e.g. `"12px 6px"`) |
| `sizes.groupGap` | string | `"12px"` | Space between groups |
| `sizes.groupBorderRadius` | string | `"4px"` | Group corner rounding |
| `sizes.widgetGap` | string | `"12px"` | Space between widgets in group |
---
## EVOLV Key Rules
1. **12-column grid** with 4-col groups gives a clean 3-column layout at desktop.
2. Set `height: "1"` on groups to let them auto-size (content determines height).
3. Use `order` on groups to control left-to-right placement within a row.
4. For compact dashboards: theme `rowHeight: "compact"`, `pagePadding: "6px"`, `groupGap: "6px"`, `widgetGap: "6px"`.
5. Widget `order` within a group determines top-to-bottom flow (lower = higher).
6. Gauge sizing guide: tank gauge `width:2, height:4` + 3/4 gauge `width:2, height:3` fits well in a 4-col group alongside a status text `width:4, height:1`.

@@ -0,0 +1,108 @@
# FlowFuse `ui-gauge` Manual (EVOLV Reference)
Source: https://dashboard.flowfuse.com/nodes/widgets/ui-gauge.html
## Gauge Types
| `gtype` value | Visual | Best For |
|---------------|--------|----------|
| `gauge-tile` | Compact square with value | KPI tiles |
| `gauge-battery` | Horizontal battery bar | Charge / capacity |
| `gauge-tank` | Vertical tank with fill gradient | Liquid levels |
| `gauge-half` | 180° arc | Setpoint / range display |
| `gauge-34` | 270° arc | Primary process gauge |
## Properties
| Property | Type | Dynamic | Notes |
|----------|------|---------|-------|
| `group` | ref | No | Parent ui-group |
| `width` | int | No | Columns (max = group width) |
| `height` | int | No | Row units |
| `gtype` | string | Yes | See table above |
| `gstyle` | string | Yes | `"Needle"` or `"Rounded"` (half/3-4 only) |
| `min` | number | Yes | Range minimum |
| `max` | number | Yes | Range maximum |
| `segments` | array | Yes | `[{color: "#hex", from: number}, …]` |
| `title` | string | Yes | Label above gauge |
| `prefix` | string | Yes | Before value (half/3-4 only) |
| `suffix` | string | Yes | After value (half/3-4 only) |
| `units` | string | Yes | Small text below value (half/3-4 only) |
| `icon` | string | Yes | Material Design icon (half/3-4 only) |
| `sizeGauge` | int | No | Arc thickness px |
| `sizeGap` | int | No | Gap between arc & segments px |
| `sizeSegments` | int | No | Segment ring thickness px |
## Input
`msg.payload` — numeric value to display.
## Dynamic Properties (`msg.ui_update`)
```js
msg.ui_update = {
label: "Tank A",
gtype: "gauge-tank",
gstyle: "Rounded",
min: 0, max: 100,
segments: [{color:"#f44336", from:0}, {color:"#4caf50", from:25}, {color:"#f44336", from:90}],
prefix: "", suffix: "%", units: "fill"
};
```
## Segments
Array of `{color, from}` objects sorted ascending by `from`. Each segment colors the range from its `from` up to the next segment's `from` (or `max`).
**Tank default segments** (auto-applied when switching to tank type):
```json
[{"color":"#A8F5FF","from":0},{"color":"#55DBEC","from":15},
{"color":"#53B4FD","from":35},{"color":"#2397D1","from":50}]
```
## CSS Selectors
| Selector | Target |
|----------|--------|
| `.nrdb-ui-gauge-value span` | Central value text |
| `.nrdb-ui-gauge-value label` | Unit label |
| `.nrdb-ui-gauge-value i` | Icon |
| `.nrdb-ui-gauge #limits` | Min/max labels |
## EVOLV Key Rules
1. **Tank gauge** for basin level: set `gtype:"gauge-tank"`, `min:0`, `max:<basin height>`, custom segments by safety thresholds.
2. **3/4 gauge** for fill %: set `gtype:"gauge-34"`, `gstyle:"Rounded"`, `min:0`, `max:100`, segments for low/normal/high.
3. Recommended sizing: tank `width:2, height:4`; 3/4 arc `width:2, height:3`.
4. Send plain numeric `msg.payload` — no topic needed (gauge has no series concept).
5. Segments must be provided as array even for a single color range.
## Node JSON Example (tank)
```json
{
"id": "demo_gauge_tank_west",
"type": "ui-gauge",
"z": "demo_tab_dashboard",
"group": "demo_ui_grp_ps_west",
"name": "West Tank Level",
"gtype": "gauge-tank",
"gstyle": "Rounded",
"title": "Level",
"units": "m",
"prefix": "",
"suffix": "m",
"min": 0, "max": 4,
"segments": [
{"color":"#f44336","from":0},
{"color":"#ff9800","from":0.3},
{"color":"#2196f3","from":1.0},
{"color":"#ff9800","from":2.5},
{"color":"#f44336","from":3.2}
],
"width": 2, "height": 4,
"order": 2,
"x": 700, "y": 400,
"wires": []
}
```

@@ -0,0 +1,112 @@
# FlowFuse `ui-template` Manual (EVOLV Reference)
Source: https://dashboard.flowfuse.com/nodes/widgets/ui-template.html
## Purpose
Custom Vue 3 / Vuetify / HTML widget. Full scripting, scoped CSS, send/receive messages.
## Scopes
| Scope | Renders | Use Case |
|-------|---------|----------|
| Widget (Group) | Inside a ui-group | Custom gauges, tables, controls |
| Widget (Page) | On page, outside groups | Floating overlays, full-width banners |
| Widget (UI) | On every page | Global headers, footers |
| CSS (All Pages) | N/A — injects `<style>` | Global CSS overrides |
| CSS (Single Page) | N/A — injects `<style>` | Page-specific CSS |
## Properties
| Property | Type | Dynamic | Notes |
|----------|------|---------|-------|
| `group` | ref | No | Parent ui-group (Group scope) |
| `page` | ref | No | Parent ui-page (Page scope) |
| `width` | int | No | Columns |
| `height` | int | No | Row units |
| `format` | string | Yes | Vue template string |
| `storeOutMessages` | bool | No | Persist last output for late joiners |
| `passthru` | bool | No | Forward input to output |
| `templateScope` | string | No | `"widget:group"`, `"widget:page"`, `"widget:ui"`, `"page:style"`, `"site:style"` |
## Template Structure
```html
<template>
<v-btn @click="send({payload: 'clicked'})">Click</v-btn>
<p>Value: {{ msg.payload }}</p>
</template>
<script>
export default {
data() { return { local: 0 }; },
watch: {
msg(val) { this.local = val.payload; }
},
methods: {
doSend() { this.send({payload: this.local}); }
},
mounted() { /* one-time setup */ },
unmounted() { /* cleanup */ }
};
</script>
<style scoped>
p { color: #2196f3; }
</style>
```
## Built-in Variables
| Variable | Description |
|----------|-------------|
| `id` | This node's unique ID |
| `msg` | Latest received message (reactive) |
| `$socket` | Socket.io client for custom events |
## Sending Messages
```js
this.send({ payload: 42, topic: "my-topic" });
this.submit(); // sends FormData from <form>
```
## Receiving Messages
**Option A** — Vue `watch`:
```js
watch: { msg(newMsg) { /* react */ } }
```
**Option B** — Socket listener:
```js
mounted() {
this.$socket.on('msg-input:' + this.id, (msg) => { /* handle */ });
}
```
## Teleports (inject into dashboard chrome)
```html
<Teleport to="#app-bar-actions">
<v-btn>Custom Button</v-btn>
</Teleport>
```
Targets: `#app-bar-title`, `#app-bar-actions`.
## Vuetify Components
All Vuetify 3 components are available without import: `<v-btn>`, `<v-card>`, `<v-slider>`, `<v-data-table>`, etc.
## Dynamic Properties (`msg.ui_update`)
```js
msg.ui_update = { format: "<p>New template content</p>" };
```
## EVOLV Key Rules
1. Use templates sparingly — prefer built-in widgets when they fit.
2. For complex custom visualizations (SVG P&ID, animated schematics), template is the right choice.
3. Always use `<style scoped>` to avoid leaking CSS to other widgets.
4. Prefer `watch: { msg }` over socket listeners for simpler code.
5. Keep templates under 100 lines — split complex UIs into multiple template nodes.

@@ -0,0 +1,72 @@
# FlowFuse `ui-text` Manual (EVOLV Reference)
Source: https://dashboard.flowfuse.com/nodes/widgets/ui-text.html
## Purpose
Read-only text display widget. Shows a label + value. Supports HTML in payload.
## Properties
| Property | Type | Dynamic | Notes |
|----------|------|---------|-------|
| `group` | ref | No | Parent ui-group |
| `width` | int | No | Columns |
| `height` | int | No | Row units |
| `label` | string | Yes | Left-side label text |
| `format` | string | No | Template: `{{msg.payload}}` or `{{msg.myProp}}` |
| `layout` | string | Yes | See layout options below |
| `style` | bool | No | Enable custom font/size/color |
| `font` | string | Yes | Font family (when style enabled) |
| `fontSize` | string | Yes | e.g. `"16px"`, `"1.2rem"` |
| `color` | string | Yes | Text color (when style enabled) |
## Layout Options
| Value | Description |
|-------|-------------|
| `row-left` | Label and value left-aligned |
| `row-center` | Centered on row |
| `row-right` | Right-aligned |
| `row-spread` | Label left, value right |
| `col-center` | Stacked vertically, centered |
## Input
`msg.payload` — string or number. HTML is rendered if present.
## Dynamic Properties (`msg.ui_update`)
```js
msg.ui_update = {
label: "New Label",
layout: "row-spread",
font: "monospace",
fontSize: "14px",
color: "#ff5722"
};
```
Also: `msg.class` — add CSS class to the widget container.
## Value Formatting
The `format` field supports template syntax and JSONata:
- `{{msg.payload}}` — raw value
- `{{msg.myProp}}` — any msg property
- JSONata in Value property: `$round(payload, 1)`
## HTML Support
`msg.payload` can contain HTML tags. Useful for inline formatting:
```js
msg.payload = '<b>Active</b> — 3 pumps online';
```
## EVOLV Key Rules
1. Use `layout: "row-spread"` for status lines (label left, value right).
2. For multi-field status, build a formatted string in the Function node and send as single payload.
3. Keep text widgets `height: 1` for compact status rows.
4. Use `format: "{{msg.payload}}"` (the default) — keeps it simple.
5. For direction arrows and styled status, use HTML in payload: `↑ filling | 142 m³/h | 35 min`.

@@ -0,0 +1,49 @@
# FlowFuse Dashboard 2.0 — Widget Catalog
Source: https://dashboard.flowfuse.com/
## Widgets (22 types)
| Widget | Type String | Description |
|--------|-------------|-------------|
| Audio | `ui-audio` | Play audio files or TTS in browser |
| Button | `ui-button` | Clickable button, sends msg on click/pointerdown/pointerup |
| Button Group | `ui-button-group` | Multiple buttons rendered together as toggle/selection |
| Chart | `ui-chart` | Line, bar, scatter, pie, histogram — backed by eCharts |
| Control | `ui-control` | Programmatic page navigation and group visibility control |
| Dropdown | `ui-dropdown` | Select one or many options from a dropdown list |
| Event | `ui-event` | Fires msg on browser-side events (page load, resize, etc.) |
| File Input | `ui-file-input` | File upload from browser to Node-RED |
| Form | `ui-form` | Multi-field input form with submit action |
| Gauge | `ui-gauge` | Numeric display as tile, battery, tank, half-arc, or 3/4-arc |
| Markdown | `ui-markdown` | Render markdown/HTML content |
| Notification | `ui-notification` | Toast / snackbar / alert popups |
| Number Input | `ui-number-input` | Numeric input field with optional range/step |
| Progress | `ui-progress` | Linear or circular progress bar |
| Radio Group | `ui-radio-group` | Select one option from radio buttons |
| Slider | `ui-slider` | Horizontal or vertical slider control |
| Spacer | `ui-spacer` | Empty cell for layout positioning |
| Switch | `ui-switch` | On/off toggle switch |
| Table | `ui-table` | Paginated data table with sorting/selection |
| Template | `ui-template` | Custom Vue/Vuetify/HTML with full scripting |
| Text | `ui-text` | Read-only text display, supports HTML |
| Text Input | `ui-text-input` | Editable text field (text, password, email, etc.) |
## Config Nodes (4 types)
| Node | Type String | Purpose |
|------|-------------|---------|
| Base | `ui-base` | Root dashboard instance — path, nav style, header |
| Theme | `ui-theme` | Colors, sizing, spacing, group styling |
| Page | `ui-page` | Dashboard page — path, layout (grid/fixed/notebook/tabs), breakpoints |
| Group | `ui-group` | Widget container — width, height, order within a page |
## See Also
- [ui-gauge manual](flowfuse-ui-gauge-manual.md)
- [ui-text manual](flowfuse-ui-text-manual.md)
- [ui-template manual](flowfuse-ui-template-manual.md)
- [ui-button manual](flowfuse-ui-button-manual.md)
- [ui-chart manual](flowfuse-ui-chart-manual.md)
- [ui-config manual](flowfuse-ui-config-manual.md)
- [Dashboard layout notes](flowfuse-dashboard-layout-manual.md)

@@ -0,0 +1,29 @@
# Node-RED Function Node Patterns (EVOLV Summary)
Based on: https://nodered.org/docs/user-guide/writing-functions
## Return Semantics
- `return msg;` sends to output 1.
- `return [msg1, msg2, ...];` sends one message per output position.
- Use `null` for outputs that should not emit.
Examples:
- `return [msg, null, null];` -> output 1 only
- `return [null, msg, null];` -> output 2 only
## Message Mutation Pattern
- Start with the incoming `msg`, set fields (`msg.topic`, `msg.payload`), then return/send.
- This preserves message context unless there is a deliberate reason to create a new object.
## Async Function Pattern
- For asynchronous work, call `node.send(...)` and then `node.done()`.
- Avoid returning a message when output is sent asynchronously.
## EVOLV Practical Rule
- In example flows (including parsers), always align return-array position with downstream wiring.
- For dashboard parser functions, keep one stable output mapping per metric to avoid wire-level regressions.

30
Manual-NodeRED-INDEX.md Executable file

@@ -0,0 +1,30 @@
# Node-RED Manual Index
This folder summarizes official Node-RED docs that are relevant to EVOLV node development.
## Official Sources
- Creating Nodes: JavaScript file and message handling
https://nodered.org/docs/creating-nodes/node-js
- Creating Nodes: Edit dialog and node definition in `.html`
https://nodered.org/docs/creating-nodes/edit-dialog
- Working with messages
https://nodered.org/docs/user-guide/messages
- Writing Functions (return arrays, multiple outputs, async send/done)
https://nodered.org/docs/user-guide/writing-functions
## What To Check First (EVOLV)
1. Runtime routing in `src/nodeClass.js`: use explicit output arrays for multi-output nodes.
2. Input handlers: use `send` + `done` pattern from Node-RED runtime docs.
3. Function nodes in example flows: return arrays with output-position alignment.
4. Editor/runtime parity: properties in `RED.nodes.registerType(...defaults...)` must map to runtime config parsing.
5. For FlowFuse dashboard reference, see:
- `flowfuse-widgets-catalog.md` — master index of all 22 widget types
- `flowfuse-ui-chart-manual.md` — chart widget (line, bar, scatter, pie, histogram)
- `flowfuse-ui-gauge-manual.md` — gauge widget (tile, battery, tank, half, 3/4 arc)
- `flowfuse-ui-text-manual.md` — text display widget
- `flowfuse-ui-template-manual.md` — custom Vue/Vuetify template widget
- `flowfuse-ui-button-manual.md` — button widget
- `flowfuse-ui-config-manual.md` — config nodes (ui-base, ui-page, ui-group, ui-theme)
- `flowfuse-dashboard-layout-manual.md` — layout patterns and sizing rules

@@ -0,0 +1,27 @@
# Node-RED Messages and Editor/Runtime Structure (EVOLV Summary)
Sources:
- Messages: https://nodered.org/docs/user-guide/messages
- Edit dialog and node definition: https://nodered.org/docs/creating-nodes/edit-dialog
## Message Shape
- Standard message object is usually `msg`.
- `msg.payload` is the default value channel.
- `msg.topic` is used for routing/intent labeling.
## EVOLV Topic Routing
- EVOLV runtime wrappers route commands by `msg.topic` in `src/nodeClass.js`.
- Keep topic names stable unless a migration is defined (flow compatibility).
## Editor/Runtime Parity
- In `.html`, `RED.nodes.registerType(... defaults ...)` defines persisted config fields.
- Runtime parser in `src/nodeClass.js` must read and normalize the same fields.
- If a field is added/renamed in editor defaults, update runtime mapping and tests in the same change.
## Multi-Output Node Rule
- Runtime `node.send([out1, out2, out3])` must align with declared `outputs` in `.html`.
- If output meaning is `process`, `dbase`, `parent`, maintain stable index mapping across all message paths.

@@ -0,0 +1,31 @@
# Node-RED Runtime Node JS Manual (EVOLV Summary)
Based on: https://nodered.org/docs/creating-nodes/node-js
## Input Handler Contract
- Node-RED calls runtime handlers as `function(msg, send, done)`.
- For compatibility, custom nodes should support fallback behavior when `send` is not provided.
- Use `done()` on successful completion and `done(err)` on failure.
## Output Routing Rules
- For single-output nodes: `send(msg)` sends on output 1.
- For multi-output nodes: pass an array with one slot per output, for example:
- `[msg, null, null]` => output 1 only
- `[null, msg, null]` => output 2 only
- Always construct the outbound message first, then send it.
## EVOLV Runtime Pattern
Recommended runtime input structure for `src/nodeClass.js`:
1. Normalize `send` fallback.
2. Wrap `switch(msg.topic)` in `try/catch`.
3. Route response messages to explicit output index with array format.
4. Call `done()`/`done(err)` consistently.
## Common Failure Mode
- Sending a bare object in a multi-output context can hide intended port routing.
- In EVOLV nodes with 3 outputs (`process`, `dbase`, `parent`), use explicit arrays for deterministic wiring behavior.

202
Telemetry.md Executable file

@@ -0,0 +1,202 @@
# Telemetry
> **Reflects code as of `9ab9f6b` · regenerated `2026-05-11`**
What every EVOLV node emits on each of its three output ports, the InfluxDB line-protocol layout, and how the data reaches Grafana/FlowFuse.
## Three-port model
```mermaid
flowchart LR
sc[specificClass<br/>tick or event]:::tier3
nc[nodeClass<br/>outputUtils.formatMsg]:::tier2
p0[(Port 0<br/>process)]:::p0
p1[(Port 1<br/>InfluxDB line)]:::p1
p2[(Port 2<br/>registration)]:::p2
sc -->|getOutput| nc
nc --> p0
nc --> p1
nc --> p2
p0 -. delta-compressed payload .-> dl[Downstream<br/>Node-RED logic]:::neutral
p1 -. line protocol .-> influx[(InfluxDB)]:::ext
p2 -. child.register .-> parent[Parent EVOLV node]:::neutral
classDef tier3 fill:#50a8d9,color:#000
classDef tier2 fill:#86bbdd,color:#000
classDef p0 fill:#86bbdd
classDef p1 fill:#a9daee
classDef p2 fill:#dddddd
classDef neutral fill:#dddddd
classDef ext fill:#fff2cc
```
## Port 0 — Process data (delta-compressed)
**Purpose:** feeds downstream Node-RED logic — dashboards, control functions, alarms.
**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.
**Example (rotatingMachine, 1 tick):**
```json
{
"topic": "rotatingMachine#pump-A",
"payload": {
"flow.predicted.downstream.default": 12.4,
"predictionConfidence": 0.87
}
}
```
Most other fields (state, pressure, mode, …) didn't change this tick — omitted.
**Trigger:** `outputUtils.checkForChanges()` compares the current `getOutput()` against the previous snapshot.
## Port 1 — Telemetry (InfluxDB line protocol)
**Purpose:** time-series storage in InfluxDB 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>
```
**Example:**
```
rotatingMachine,id=pump-A,softwareType=rotatingMachine flow_predicted_downstream=12.4,power_measured_atequipment=18.2 1714752000000000000
```
**Conventions:**
| Element | Rule |
|---|---|
| measurement (table) | The node's `softwareType` (lowercase). |
| 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). |
| timestamp | Nanoseconds. Set by `outputUtils` from the node's clock. |
See [InfluxDB schema design](concepts/influxdb-schema-design) for the cardinality discipline.
## Port 2 — Registration / control
**Purpose:** upward `child.register` at startup; later, internal control msgs (the registry-driven command replies).
**Shape:**
```json
{
"topic": "child.register",
"payload": {
"ref": <node reference>,
"softwareType": "machine",
"config": { ... }
}
}
```
**Trigger:** the nodeClass adapter emits `child.register` on `init` if a `parent` is configured. The parent's `commandRegistry` dispatches into `ChildRouter.onRegister(...)`.
## The output composition pipeline
```mermaid
sequenceDiagram
participant tick as Tick (1 Hz)
participant sc as specificClass
participant mc as MeasurementContainer
participant ou as outputUtils
participant ports as Ports 0 / 1
tick->>sc: tick()
sc->>sc: concern modules update mc + state
sc->>ou: getOutput() snapshot
ou->>ou: diff vs last
alt no change
ou-->>sc: skip
else change
ou->>ports: Port 0 — JSON delta
ou->>ports: Port 1 — line protocol
end
```
`outputUtils` is the single place the platform serialises state. Never write directly to `node.send` from specificClass — go through `outputUtils.formatMsg`.
## InfluxDB layout
Recommended schema for EVOLV's data:
| InfluxDB element | Maps to |
|---|---|
| Database / bucket | One per plant (or per environment: `evolv_dev`, `evolv_prod`). |
| Measurement (table) | Node softwareType (`rotatingMachine`, `pumpingStation`, …). |
| Tags | `id` (instance id), `softwareType`, `area`, `processCell`, `unit` (for hierarchical drill-down). |
| Fields | Numeric series — every key from `getOutput()` that has a numeric value, flattened with `_`. |
| Retention | Hot bucket: 7 days @ 1 s. Cold bucket: 1 year @ 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.
## 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).
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).
- A trend-feeder function splits Port-0 deltas into per-series outputs, each output wired to one chart.
- The chart's `category: "topic"` + `categoryType: "msg"` plots one series per unique `msg.topic`.
```mermaid
flowchart LR
p0[(Port 0)]:::p0
split[trend-feeder<br/>function (N outputs)]:::tier2
chart1[ui-chart: flow]:::neutral
chart2[ui-chart: power]:::neutral
p0 --> split
split --> chart1
split --> chart2
classDef p0 fill:#86bbdd
classDef tier2 fill:#86bbdd,color:#000
classDef neutral fill:#dddddd
```
## Grafana dashboard provisioning
`dashboardAPI` consumes registrations and emits Grafana dashboard JSON via HTTP. Wiring:
```mermaid
flowchart LR
evolv[EVOLV node<br/>any softwareType]:::tier3
dash[dashboardAPI]:::util
grafana[(Grafana HTTP API<br/>POST /api/dashboards/db)]:::ext
evolv -->|child.register| dash
dash -->|composed JSON| grafana
classDef tier3 fill:#50a8d9,color:#000
classDef util fill:#dddddd
classDef ext fill:#fff2cc
```
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.
## Common debug recipes
| Symptom | First check |
|---|---|
| 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. |
| 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). |
| 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. |
| Grafana dashboard not created on plant boot | Inspect dashboardAPI's HTTP response. Check the bearer token + base URL in its config. |
## Related pages
- [Architecture](Architecture) — output port wiring in the 3-tier code
- [Topic-Conventions](Topic-Conventions) — what topics map to what fields
- [InfluxDB schema design](concepts/influxdb-schema-design) — cardinality discipline

185
Topic-Conventions.md Executable file

@@ -0,0 +1,185 @@
# Topic Conventions
> **Reflects code as of `9ab9f6b` · regenerated `2026-05-11`**
Naming rules, unit policy, and S88 colour palette. Source of truth: `.claude/refactor/CONTRACTS.md` §1.
## Topic prefixes
Every topic is `<prefix>.<verb>` lowercase. Five prefixes only.
```mermaid
flowchart LR
ui[UI / parent / driver]:::neutral
node[Node]:::tier3
child[Child]:::tier1
ui -->|set.x / cmd.x| node
node -->|evt.x| ui
child -->|data.x| node
node -->|data.x| child
child -->|child.register| node
classDef neutral fill:#dddddd
classDef tier3 fill:#50a8d9,color:#000
classDef tier1 fill:#a9daee,color:#000
```
| Prefix | Direction | Semantics | Examples |
|---|---|---|---|
| `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:**
- ❌ 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`.
- ❌ Per-node prefixes (`pump.set.demand`). The prefix is the *kind*, not the *target*.
## Alias deprecation
Legacy topic names (pre-refactor) are still accepted as aliases. The current alias map per node lives in `src/commands/index.js`. Common aliases:
| Canonical | Legacy aliases |
|---|---|
| `set.mode` | `setMode` |
| `set.demand` | `Qd`, `setDemand` |
| `cmd.startup` | `execSequence` with `payload.action='startup'` |
| `cmd.shutdown` | `execSequence` with `payload.action='shutdown'` |
| `child.register` | `registerChild` |
| `data.pressure` | `pressure` |
| `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.
## 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).
```mermaid
flowchart LR
ui[UI message<br/>e.g. 50 m³/h]:::neutral
coerce[unit coercion<br/>m³/h → m³/s]:::tier1
sc[specificClass<br/>canonical m³/s]:::tier3
out[output<br/>renders back to m³/h]:::tier2
ui --> coerce --> sc --> out
classDef neutral fill:#dddddd
classDef tier1 fill:#a9daee,color:#000
classDef tier3 fill:#50a8d9,color:#000
classDef tier2 fill:#86bbdd,color:#000
```
| Quantity | Canonical (internal) | Common output |
|---|---|---|
| Flow | `m3/s` | `m3/h`, `l/s`, `gpm` |
| Pressure | `Pa` | `bar`, `mbar`, `kPa` |
| Power | `W` | `kW`, `MW` |
| Temperature | `K` | `degC`, `degF` |
| Level | `m` | `m`, `cm` |
| Volume | `m3` | `m3`, `l` |
**Rule:** anywhere in `specificClass`, treat values as canonical. Conversion happens at the boundary (input coercion + output formatting).
## S88 colour palette
```mermaid
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
UT -.- A
classDef area fill:#0f52a5,color:#fff
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
- [Architecture](Architecture) — generalFunctions API surface
- [Telemetry](Telemetry) — Port-1 InfluxDB schema (where these conventions appear in stored data)
- [Topology-Patterns](Topology-Patterns) — what topics flow where

248
Topology-Patterns.md Executable file

@@ -0,0 +1,248 @@
# Topology Patterns
> **Reflects code as of `9ab9f6b` · regenerated `2026-05-11`**
Typical plant configurations and how nodes wire together. Each pattern is **verified** against the corresponding nodes' `configure()` declarations.
## 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.
```mermaid
flowchart TB
subgraph PC["Process Cell"]
ps[pumpingStation]:::pc
end
subgraph UN["Unit"]
mgc[machineGroupControl]:::unit
end
subgraph EM["Equipment"]
rmA[rotatingMachine A]:::equip
rmB[rotatingMachine B]:::equip
rmC[rotatingMachine C]:::equip
end
subgraph CM["Control Module"]
ml[measurement: level]:::ctrl
mfin[measurement: inflow]:::ctrl
mpA[measurement: pressure A]:::ctrl
mpB[measurement: pressure B]:::ctrl
mpC[measurement: pressure C]:::ctrl
end
ps --> mgc
mgc --> rmA
mgc --> rmB
mgc --> rmC
ml -. data .-> ps
mfin -. data .-> ps
mpA -. data .-> rmA
mpB -. data .-> rmB
mpC -. data .-> rmC
classDef pc fill:#0c99d9,color:#fff
classDef unit fill:#50a8d9,color:#000
classDef equip fill:#86bbdd,color:#000
classDef ctrl fill:#a9daee,color:#000
```
**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:**
- 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.
## Pattern 2 — Reactor / settler train with aeration
Biological treatment line. Reactor runs ASM kinetics, diffuser drives O₂ transfer, settler clarifies effluent and returns sludge via a return pump.
```mermaid
flowchart TB
subgraph UN["Unit"]
reactor[reactor]:::unit
settler[settler]:::unit
end
subgraph EM["Equipment"]
diff[diffuser]:::equip
rp[rotatingMachine<br/>return pump]:::equip
end
subgraph CM["Control Module"]
mt[measurement: temperature]:::ctrl
mdo[measurement: dissolved O₂]:::ctrl
mts[measurement: TSS]:::ctrl
end
reactor ==stateChange==> settler
diff -. OTR data .-> reactor
settler -->|return pump child| rp
mt -. data .-> reactor
mdo -. data .-> reactor
mts -. data .-> settler
mdo -. data .-> diff
classDef unit fill:#50a8d9,color:#000
classDef equip fill:#86bbdd,color:#000
classDef ctrl fill:#a9daee,color:#000
```
**Data flow:**
- `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:**
- Reactor supports two kinetics engines: CSTR (continuous-stirred tank) and PFR (plug-flow). Set via `config.reactor_type`.
- 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
Multi-valve flow distribution. VGC computes per-valve K_v shares to satisfy a target distribution while respecting upstream flow availability.
```mermaid
flowchart TB
subgraph PC["Process Cell"]
ps[pumpingStation<br/>upstream flow source]:::pc
end
subgraph UN["Unit"]
vgc[valveGroupControl]:::unit
end
subgraph EM["Equipment"]
vA[valve A]:::equip
vB[valve B]:::equip
vC[valve C]:::equip
end
ps -. flow source .-> vgc
vgc --> vA
vgc --> vB
vgc --> vC
classDef pc fill:#0c99d9,color:#fff
classDef unit fill:#50a8d9,color:#000
classDef equip fill:#86bbdd,color:#000
```
**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".
## 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.
```mermaid
flowchart TB
subgraph UN["Unit"]
monster[monster]:::unit
end
subgraph CM["Control Module"]
mflow[measurement: flow<br/>assetType MUST be 'flow']:::ctrl
mq[measurement: any quality<br/>e.g. NH₄, COD]:::ctrl
end
mflow -. data .-> monster
mq -. data .-> monster
classDef unit fill:#50a8d9,color:#000
classDef ctrl fill:#a9daee,color:#000
```
**Gotchas:**
- `measurement.config.asset.type` MUST be `"flow"` exactly — `"flow-electromagnetic"` or any sub-type is silently ignored by monster's child router.
- `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.)
## 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.
```mermaid
flowchart LR
subgraph EVOLV["EVOLV process nodes"]
ps[pumpingStation]:::pc
mgc[machineGroupControl]:::unit
rm[rotatingMachine]:::equip
end
subgraph UT["Utility"]
dash[dashboardAPI]:::util
end
grafana[(Grafana<br/>HTTP API)]:::ext
ps -. child.register .-> dash
mgc -. child.register .-> dash
rm -. child.register .-> dash
dash -->|POST /api/dashboards/db| grafana
classDef pc fill:#0c99d9,color:#fff
classDef unit fill:#50a8d9,color:#000
classDef equip fill:#86bbdd,color:#000
classDef util fill:#dddddd,color:#000
classDef ext fill:#fff2cc,color:#000
```
**Notes:**
- `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.
## Putting it all together — example plant
A small WWTP combining all patterns:
```mermaid
flowchart TB
subgraph PC["Process Cell"]
ps1[pumpingStation<br/>inlet lift]:::pc
ps2[pumpingStation<br/>RAS pumping]:::pc
end
subgraph UN["Unit"]
mgc1[MGC inlet]:::unit
mgc2[MGC RAS]:::unit
vgc[VGC effluent split]:::unit
r1[reactor aerobic]:::unit
s1[settler]:::unit
mon[monster<br/>composite sampler]:::unit
end
subgraph EM["Equipment"]
rm1[pump A]:::equip
rm2[pump B]:::equip
rm3[RAS pump]:::equip
d1[diffuser]:::equip
v1[valve 1]:::equip
v2[valve 2]:::equip
end
ps1 --> mgc1
mgc1 --> rm1
mgc1 --> rm2
ps2 --> mgc2
mgc2 --> rm3
r1 ==stateChange==> s1
s1 -->|return pump| rm3
d1 -. OTR .-> r1
ps2 -. flow source .-> vgc
vgc --> v1
vgc --> v2
classDef pc fill:#0c99d9,color:#fff
classDef unit fill:#50a8d9,color:#000
classDef equip fill:#86bbdd,color:#000
```
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.
## 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.
- ❌ A `diffuser → reactor` child registration. Diffuser emits OTR via its emitter; reactor subscribes. No `child.register` handshake.
-`measurement` parented under `dashboardAPI`. dashboardAPI accepts any node for Grafana provisioning, but `measurement` registers with the **process** node it's monitoring, not with dashboardAPI.
## Related pages
- [Home](Home) — top-level node map
- [Architecture](Architecture) — 3-tier code structure + generalFunctions API
- [Topic-Conventions](Topic-Conventions) — what topics flow between nodes

45
_Sidebar.md Executable file

@@ -0,0 +1,45 @@
### EVOLV Wiki
**Start here**
- [Home](Home)
- [Getting Started](Getting-Started)
**Reference**
- [Architecture](Architecture)
- [Topology Patterns](Topology-Patterns)
- [Topic Conventions](Topic-Conventions)
- [Telemetry](Telemetry)
- [Glossary](Glossary)
**Per-node wikis**
- [pumpingStation](https://gitea.wbd-rd.nl/RnD/pumpingStation/wiki/Home)
- [machineGroupControl](https://gitea.wbd-rd.nl/RnD/machineGroupControl/wiki/Home)
- [valveGroupControl](https://gitea.wbd-rd.nl/RnD/valveGroupControl/wiki/Home)
- [reactor](https://gitea.wbd-rd.nl/RnD/reactor/wiki/Home)
- [settler](https://gitea.wbd-rd.nl/RnD/settler/wiki/Home)
- [monster](https://gitea.wbd-rd.nl/RnD/monster/wiki/Home)
- [rotatingMachine](https://gitea.wbd-rd.nl/RnD/rotatingMachine/wiki/Home)
- [valve](https://gitea.wbd-rd.nl/RnD/valve/wiki/Home)
- [diffuser](https://gitea.wbd-rd.nl/RnD/diffuser/wiki/Home)
- [measurement](https://gitea.wbd-rd.nl/RnD/measurement/wiki/Home)
- [dashboardAPI](https://gitea.wbd-rd.nl/RnD/dashboardAPI/wiki/Home)
- [generalFunctions](https://gitea.wbd-rd.nl/RnD/generalFunctions/wiki/Home)
**Concepts** (domain knowledge)
- [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)
- [BEP gravitation](Finding-BEP-Gravitation-Proof)
- [Curve non-convexity](Finding-Curve-Non-Convexity)
- [NCog behaviour](Finding-NCog-Behavior)
- [Pump switching stability](Finding-Pump-Switching-Stability)
**Archive**
- [Archive index](Archive)