# 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[".js
(entry — registers node type with RED)"]:::tier1 nc["src/NodeClass.js
(nodeClass — Node-RED adapter)"]:::tier2 sc["src/SpecificClass.js
(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 (`.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: `...`. | | `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/.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
process data)]:::p0 p1[(Port 1
InfluxDB line)]:::p1 p2[(Port 2
registration / control)]:::p2 sc --> p0 sc --> p1 sc --> p2 p0 -.-> dn1[downstream Node-RED nodes
dashboards, function nodes] p1 -.-> influx[(InfluxDB)] p2 -.-> parent[parent EVOLV node
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(, 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
orchestration only"] subgraph concerns["Concern subdirs (per-node)"] c1[basin/ or curves/ or kinetics/
physics / math] c2[state/
FSM transitions] c3[dispatch/ or safety/
action / guard logic] c4[commands/
topic → handler descriptors] c5[io/
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