wiki: crisp overhaul — no decoration emoji, all 9 master pages refactored

Source-tree mirror of EVOLV.wiki.git refactor (27a42ee on wiki.git):

- 7 master pages rewritten with clean design (Home, Architecture,
  Topology-Patterns, Topic-Conventions, Telemetry, Getting-Started,
  Glossary). Tables and Mermaid for visuals, gitea alert callouts for
  warnings, shields badges for metadata only. No emoji as decoration.
- Archive.md becomes a removal-changelog pointing readers to git
  history and to the successor pages.
- _Sidebar.md updated to navigate the new flat-name layout.
- Concept / finding / manual pages: uniform mini-header (badges +
  "reference page" callout) added without rewriting domain content.
- Every internal link now uses the flat naming that resolves on the
  live gitea wiki (Concept-ASM-Models, Finding-BEP-..., etc.).

On wiki.git: 29 Archive-* pages hard-deleted (the git history
preserves them; Archive.md documents the removal).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
znetsixe
2026-05-11 22:24:51 +02:00
parent 2ccc8aea9e
commit 5ae8788fd7
33 changed files with 1491 additions and 729 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,12 @@
### EVOLV Wiki
**Start here**
- [Home](Home)
- [Getting Started](Getting-Started)
**Reference**
- [Architecture](Architecture)
- [Topology Patterns](Topology-Patterns)
- [Topic Conventions](Topic-Conventions)
@@ -12,6 +14,7 @@
- [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)
@@ -25,21 +28,39 @@
- [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)
**Domain concepts**
**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)
- [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 — Sensors](Concept-Signal-Processing-Sensors)
- [InfluxDB Schema Design](Concept-InfluxDB-Schema-Design)
- [Wastewater Compliance NL](Concept-Wastewater-Compliance-NL)
- [OT Security IEC 62443](Concept-OT-Security-IEC62443)
**Operations findings**
- [BEP Gravitation Proof](Finding-BEP-Gravitation-Proof)
- [Curve Non-Convexity](Finding-Curve-Non-Convexity)
- [NCog Behaviour](Finding-NCog-Behavior)
- [Pump Switching Stability](Finding-Pump-Switching-Stability)
**Node-RED / FlowFuse manuals**
- [Manual Index](Manual-NodeRED-INDEX)
- [Runtime — Node.js](Manual-NodeRED-Runtime-Node-Js)
- [Function Node Patterns](Manual-NodeRED-Function-Node-Patterns)
- [Messages and Editor Structure](Manual-NodeRED-Messages-And-Editor-Structure)
- [FlowFuse ui-chart](Manual-NodeRED-Flowfuse-Ui-Chart-Manual)
- [FlowFuse ui-button](Manual-NodeRED-Flowfuse-Ui-Button-Manual)
- [FlowFuse ui-gauge](Manual-NodeRED-Flowfuse-Ui-Gauge-Manual)
- [FlowFuse ui-text](Manual-NodeRED-Flowfuse-Ui-Text-Manual)
- [FlowFuse ui-template](Manual-NodeRED-Flowfuse-Ui-Template-Manual)
- [FlowFuse ui-config](Manual-NodeRED-Flowfuse-Ui-Config-Manual)
- [Dashboard Layout](Manual-NodeRED-Flowfuse-Dashboard-Layout-Manual)
- [Widgets Catalog](Manual-NodeRED-Flowfuse-Widgets-Catalog)
**Archive**
- [Archive index](Archive)
- [Archive](Archive)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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