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>
7.9 KiB
Architecture
Reflects code as of
9ab9f6b· regenerated2026-05-11
How every EVOLV node is structured, and what the shared generalFunctions library provides.
The 3-tier node pattern
flowchart LR
rt["Node-RED runtime"]:::neutral
subgraph node["Custom node (one folder under nodes/)"]
entry["<NodeName>.js<br/>(entry — registers node type with RED)"]:::tier1
nc["src/<NodeName>NodeClass.js<br/>(nodeClass — Node-RED adapter)"]:::tier2
sc["src/<NodeName>SpecificClass.js<br/>(specificClass — pure domain logic)"]:::tier3
end
rt -->|RED.nodes.registerType| entry
entry -->|new| nc
nc -->|new + configure()| sc
classDef neutral fill:#dddddd,color:#000
classDef tier1 fill:#a9daee,color:#000
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 → for the full 34-row API table.
Output ports
Every EVOLV node emits on three ports:
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 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 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().
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
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
.claude/refactor/CONTRACTS.md— every API shape this wiki abstracts over..claude/refactor/CONVENTIONS.md— code style, file size, naming..claude/refactor/MODULE_SPLIT.md— concern layout per node.- One node's
wiki/Home.md(pumpingStation is the most mature pilot — start there). - The corresponding
src/folder, top-down: specificClass → concern modules.
Related pages
- Topology-Patterns — typical plant configurations
- Topic-Conventions — naming and units
- Telemetry — Port-1 InfluxDB schema
- Getting-Started — hands-on first run