Complete redesign of the platform-level wiki. Previous Home.md had a broken Mermaid diagram (showed pumpingStation → valveGroupControl as a parent/child edge, which isn't in any configure() declaration). Audit of all 12 specificClass.js configure() calls drives the new ground-truth hierarchy. New pages: - Home.md (rewritten — accurate mermaid, full node + concept index) - Architecture.md (3-tier code structure, generalFunctions API surface, child-registration sequence) - Topology-Patterns.md (5 verified plant configurations + worked example) - Topic-Conventions.md (set./cmd./evt./data./child. + unit policy + S88 palette + measurement key shape + status badge + HealthStatus) - Telemetry.md (Port 0/1/2 contracts + InfluxDB line-protocol layout + FlowFuse charts + Grafana provisioning) - Getting-Started.md (clone, install, Docker vs local, first example) - Glossary.md (S88, EVOLV runtime, WWTP, pumps, control, project terms) - _Sidebar.md (gitea wiki navigation) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
171 lines
7.9 KiB
Markdown
171 lines
7.9 KiB
Markdown
# Architecture
|
|
|
|
> **Reflects code as of `9ab9f6b` · regenerated `2026-05-11`**
|
|
|
|
How every EVOLV node is structured, and what the shared `generalFunctions` library provides.
|
|
|
|
## The 3-tier node pattern
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
rt["Node-RED runtime"]:::neutral
|
|
subgraph node["Custom node (one folder under nodes/)"]
|
|
entry["<NodeName>.js<br/>(entry — registers node type with RED)"]:::tier1
|
|
nc["src/<NodeName>NodeClass.js<br/>(nodeClass — Node-RED adapter)"]:::tier2
|
|
sc["src/<NodeName>SpecificClass.js<br/>(specificClass — pure domain logic)"]:::tier3
|
|
end
|
|
rt -->|RED.nodes.registerType| entry
|
|
entry -->|new| nc
|
|
nc -->|new + configure()| sc
|
|
|
|
classDef neutral fill:#dddddd,color:#000
|
|
classDef tier1 fill:#a9daee,color:#000
|
|
classDef tier2 fill:#86bbdd,color:#000
|
|
classDef tier3 fill:#50a8d9,color:#000
|
|
```
|
|
|
|
| Tier | Owns | Touches RED.* API? | Tested by |
|
|
|---|---|---|---|
|
|
| entry (`<NodeName>.js`) | Type registration, HTTP admin endpoints | yes | smoke tests |
|
|
| nodeClass (`src/...NodeClass.js`, extends `BaseNodeAdapter`) | msg routing, tick loop, output port wiring, status badge updates | yes | integration tests |
|
|
| specificClass (`src/...SpecificClass.js`, extends `BaseDomain`) | All business logic; emits via `this.emitter`; calls `this.measurements` / `this.router` | **no** — must be free of RED imports | unit tests |
|
|
|
|
**Rule:** never import Node-RED APIs in the specificClass. The specificClass is unit-testable by `new SpecificClass(config)`. If you find `RED.*` calls outside the entry/nodeClass tiers, that's a bug.
|
|
|
|
## generalFunctions — what it provides
|
|
|
|
The `nodes/generalFunctions` submodule is a plain-JS library every node depends on. Public exports (top-level `require('generalFunctions')`):
|
|
|
|
| Export | Role |
|
|
|---|---|
|
|
| `BaseDomain` | Base class for every specificClass. Owns `measurements`, `router`, `emitter`, `logger`, `unitPolicy`. |
|
|
| `BaseNodeAdapter` | Base class for every nodeClass. Wires `commandRegistry` to `node.on('input')`, owns tick loop. |
|
|
| `ChildRouter` | Declarative child-registration matcher. `router.onRegister(softwareType, handler)`, `router.onMeasurement(...)`. |
|
|
| `commandRegistry` | Topic → handler descriptor map. Owns alias resolution + unit coercion. |
|
|
| `UnitPolicy` | Per-node canonical + output units. Coerces incoming `msg.unit` to canonical. |
|
|
| `MeasurementContainer` | Chainable storage: `type(t).variant(v).position(p).value(x, ts, unit)`. Key shape: `<type>.<variant>.<position>.<childId>`. |
|
|
| `statusBadge` | Composer for `node.status({fill,shape,text})` updates. |
|
|
| `HealthStatus` | Standardised `{ level: 0..3, flags: [], message, source }` shape. |
|
|
| `LatestWinsGate` | Mutex with supersede semantics — keeps only the freshest in-flight call. |
|
|
| `logger` | Structured logger (use this; never `console.log`). |
|
|
| `configManager` | Loads JSON schemas from `src/configs/<node>.json`. |
|
|
| `MenuManager` | Dynamic editor dropdowns (asset lists). |
|
|
| `outputUtils` | Delta-compressed Port-0 + InfluxDB-line-protocol Port-1 formatting. |
|
|
|
|
See [generalFunctions Home →](https://gitea.wbd-rd.nl/RnD/generalFunctions/wiki/Home) for the full 34-row API table.
|
|
|
|
## Output ports
|
|
|
|
Every EVOLV node emits on three ports:
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
sc[specificClass]:::tier3
|
|
p0[(Port 0<br/>process data)]:::p0
|
|
p1[(Port 1<br/>InfluxDB line)]:::p1
|
|
p2[(Port 2<br/>registration / control)]:::p2
|
|
sc --> p0
|
|
sc --> p1
|
|
sc --> p2
|
|
|
|
p0 -.-> dn1[downstream Node-RED nodes<br/>dashboards, function nodes]
|
|
p1 -.-> influx[(InfluxDB)]
|
|
p2 -.-> parent[parent EVOLV node<br/>via child.register]
|
|
|
|
classDef tier3 fill:#50a8d9,color:#000
|
|
classDef p0 fill:#86bbdd
|
|
classDef p1 fill:#a9daee
|
|
classDef p2 fill:#dddddd
|
|
```
|
|
|
|
| Port | Carries | Format | Cardinality |
|
|
|---|---|---|---|
|
|
| **0** Process | Delta-compressed measurement / state snapshot for downstream Node-RED logic. | `msg.payload` = object of changed keys only. | One msg per tick when something changed. |
|
|
| **1** Telemetry | InfluxDB line-protocol strings: `measurement,tag=val field=val ts`. | `msg.payload` = `string` (or array of strings). | One msg per tick when something changed; all numeric outputs. |
|
|
| **2** Registration / control | `child.register` upward on adapter init; control replies. | `{topic, payload: nodeRef}` | At init time + on demand. |
|
|
|
|
See [Telemetry](Telemetry) for the full Port-1 schema and InfluxDB conventions.
|
|
|
|
## Topic conventions
|
|
|
|
| Prefix | Direction | Used for |
|
|
|---|---|---|
|
|
| `set.` | inbound | Set a configurable value (mode, setpoint). Idempotent. |
|
|
| `cmd.` | inbound | Trigger an action (startup, shutdown, calibrate). Has side-effects. |
|
|
| `data.` | inbound or outbound | Carries measurement data between child ↔ parent. |
|
|
| `evt.` | outbound | Signal that something happened (state change, alarm). |
|
|
| `child.` | inbound (on parent) | Child node registers itself with this parent. |
|
|
|
|
See [Topic-Conventions](Topic-Conventions) for the full list, payload shapes, alias deprecation map.
|
|
|
|
## Child registration
|
|
|
|
When a node is configured with `parent` = some other node's id, on `init()` the nodeClass emits a `child.register` message on Port 2 toward the parent. The parent's `commandRegistry` routes it into `ChildRouter`, which fires the matching `onRegister(softwareType, handler)` declared in `configure()`.
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant childNc as Child nodeClass
|
|
participant parentReg as Parent commandRegistry
|
|
participant parentRouter as Parent ChildRouter
|
|
participant parentSc as Parent specificClass
|
|
|
|
childNc->>parentReg: msg{topic: child.register, softwareType, ref}
|
|
parentReg->>parentRouter: dispatch(child.register, ref)
|
|
parentRouter->>parentRouter: match softwareType
|
|
parentRouter->>parentSc: invoke registered handler
|
|
parentSc->>parentSc: store ref, wire emitter.on(...)
|
|
```
|
|
|
|
A child is anything the parent's `configure()` declares via `router.onRegister(<softwareType>, handler)`. Examples:
|
|
|
|
| Parent | Accepts children with softwareType |
|
|
|---|---|
|
|
| pumpingStation | `measurement`, `machine`, `machinegroup`, `pumpingstation` |
|
|
| machineGroupControl | `machine`, `measurement` |
|
|
| valveGroupControl | `valve`, `machine`, `machinegroup`, `pumpingstation`, `valvegroupcontrol` (last 4 as flow sources) |
|
|
| reactor | `measurement`, `reactor` |
|
|
| settler | `measurement`, `reactor`, `machine` |
|
|
| monster | `measurement` |
|
|
| diffuser | `measurement` |
|
|
| rotatingMachine | `measurement` |
|
|
| valve | `measurement` |
|
|
| dashboardAPI | any (used for Grafana provisioning) |
|
|
|
|
## Where business logic lives
|
|
|
|
```mermaid
|
|
flowchart TB
|
|
subgraph node["A node's src/ folder"]
|
|
sc["specificClass.js<br/>orchestration only"]
|
|
subgraph concerns["Concern subdirs (per-node)"]
|
|
c1[basin/ or curves/ or kinetics/<br/>physics / math]
|
|
c2[state/<br/>FSM transitions]
|
|
c3[dispatch/ or safety/<br/>action / guard logic]
|
|
c4[commands/<br/>topic → handler descriptors]
|
|
c5[io/<br/>output composition]
|
|
end
|
|
end
|
|
sc --> c1
|
|
sc --> c2
|
|
sc --> c3
|
|
sc --> c4
|
|
sc --> c5
|
|
```
|
|
|
|
specificClass should be **stitching only** — instantiate concern modules in `configure()`, call them in `tick()` or in router handlers. Concerns are individually testable.
|
|
|
|
## Reading order for newcomers
|
|
|
|
1. `.claude/refactor/CONTRACTS.md` — every API shape this wiki abstracts over.
|
|
2. `.claude/refactor/CONVENTIONS.md` — code style, file size, naming.
|
|
3. `.claude/refactor/MODULE_SPLIT.md` — concern layout per node.
|
|
4. One node's `wiki/Home.md` (pumpingStation is the most mature pilot — start there).
|
|
5. The corresponding `src/` folder, top-down: specificClass → concern modules.
|
|
|
|
## Related pages
|
|
|
|
- [Topology-Patterns](Topology-Patterns) — typical plant configurations
|
|
- [Topic-Conventions](Topic-Conventions) — naming and units
|
|
- [Telemetry](Telemetry) — Port-1 InfluxDB schema
|
|
- [Getting-Started](Getting-Started) — hands-on first run
|