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>
9.3 KiB
Topology Patterns
Reflects code as of
9ab9f6b· regenerated2026-05-11
Typical plant configurations and how nodes wire together. Each pattern is verified against the corresponding nodes' configure() declarations.
Pattern 1 — Pumping station with grouped pumps
The canonical wet-well lift station. One basin, one demand controller (pumpingStation), one load-sharing coordinator (machineGroupControl), N pumps. Level + flow measurements feed the basin model.
flowchart TB
subgraph PC["Process Cell"]
ps[pumpingStation]:::pc
end
subgraph UN["Unit"]
mgc[machineGroupControl]:::unit
end
subgraph EM["Equipment"]
rmA[rotatingMachine A]:::equip
rmB[rotatingMachine B]:::equip
rmC[rotatingMachine C]:::equip
end
subgraph CM["Control Module"]
ml[measurement: level]:::ctrl
mfin[measurement: inflow]:::ctrl
mpA[measurement: pressure A]:::ctrl
mpB[measurement: pressure B]:::ctrl
mpC[measurement: pressure C]:::ctrl
end
ps --> mgc
mgc --> rmA
mgc --> rmB
mgc --> rmC
ml -. data .-> ps
mfin -. data .-> ps
mpA -. data .-> rmA
mpB -. data .-> rmB
mpC -. data .-> rmC
classDef pc fill:#0c99d9,color:#fff
classDef unit fill:#50a8d9,color:#000
classDef equip fill:#86bbdd,color:#000
classDef ctrl fill:#a9daee,color:#000
Data flow:
pumpingStationcomputes basin volume + level dynamics from inflow/outflow measurements.pumpingStationemits a demand setpoint downstream tomachineGroupControlon its Port 0 or viaset.demand.machineGroupControlsolves a per-pump operating point using each pump's characteristic curve + measured pressure, sendsset.setpointto eachrotatingMachine.- Each
rotatingMachineruns its own FSM (idle/warmingup/operational/coolingdown/emergencystop) and predicts flow/power from pressure + speed.
Notes:
- For a single-pump station,
pumpingStationcan registerrotatingMachinedirectly (skip the MGC) —pumpingStation'sconfigure()acceptsmachineas a child softwareType. - For two stations in series, the downstream PS can register the upstream PS as a
pumpingstationsoftwareType source.
Pattern 2 — Reactor / settler train with aeration
Biological treatment line. Reactor runs ASM kinetics, diffuser drives O₂ transfer, settler clarifies effluent and returns sludge via a return pump.
flowchart TB
subgraph UN["Unit"]
reactor[reactor]:::unit
settler[settler]:::unit
end
subgraph EM["Equipment"]
diff[diffuser]:::equip
rp[rotatingMachine<br/>return pump]:::equip
end
subgraph CM["Control Module"]
mt[measurement: temperature]:::ctrl
mdo[measurement: dissolved O₂]:::ctrl
mts[measurement: TSS]:::ctrl
end
reactor ==stateChange==> settler
diff -. OTR data .-> reactor
settler -->|return pump child| rp
mt -. data .-> reactor
mdo -. data .-> reactor
mts -. data .-> settler
mdo -. data .-> diff
classDef unit fill:#50a8d9,color:#000
classDef equip fill:#86bbdd,color:#000
classDef ctrl fill:#a9daee,color:#000
Data flow:
reactor.configure()registersmeasurement(temperature, DO) and upstreamreactor(for chained tanks).diffuseremitsdata.otron its emitter; reactor subscribes viaemitter.on('otr', …)— not a child registration, just a data subscription.reactoremitsstateChangeafter every kinetics step.settler._connectReactorsubscribes viaemitter.on('stateChange', …)and pulls effluent composition.settler.configure()acceptsreactor(the upstream),machine(return pump), andmeasurementchildren.
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
valveGroupControlupstream of an airflow valve.
Pattern 3 — Valve group on a distribution manifold
Multi-valve flow distribution. VGC computes per-valve K_v shares to satisfy a target distribution while respecting upstream flow availability.
flowchart TB
subgraph PC["Process Cell"]
ps[pumpingStation<br/>upstream flow source]:::pc
end
subgraph UN["Unit"]
vgc[valveGroupControl]:::unit
end
subgraph EM["Equipment"]
vA[valve A]:::equip
vB[valve B]:::equip
vC[valve C]:::equip
end
ps -. flow source .-> vgc
vgc --> vA
vgc --> vB
vgc --> vC
classDef pc fill:#0c99d9,color:#fff
classDef unit fill:#50a8d9,color:#000
classDef equip fill:#86bbdd,color:#000
Important detail: valveGroupControl.configure() registers four extra softwareTypes — machine, machinegroup, pumpingstation, valvegroupcontrol — not as S88 children but as flow sources. VGC uses them to read upstream flow availability when computing per-valve splits. The arrow above is child.register from pumpingStation to vgc; the semantic relationship is "VGC knows about this upstream flow producer", not "VGC controls pumpingStation".
Pattern 4 — Composite sampling
monster runs a proportional sampling program — accumulates samples in a bucket based on integrated flow. Used as a virtual sensor for downstream lab analysis.
flowchart TB
subgraph UN["Unit"]
monster[monster]:::unit
end
subgraph CM["Control Module"]
mflow[measurement: flow<br/>assetType MUST be 'flow']:::ctrl
mq[measurement: any quality<br/>e.g. NH₄, COD]:::ctrl
end
mflow -. data .-> monster
mq -. data .-> monster
classDef unit fill:#50a8d9,color:#000
classDef ctrl fill:#a9daee,color:#000
Gotchas:
measurement.config.asset.typeMUST be"flow"exactly —"flow-electromagnetic"or any sub-type is silently ignored by monster's child router.monster.config.constraints.flowmeterexists in the schema but is not forwarded bybuildDomainConfig— toggling proportional-vs-time mode has no effect at runtime. (Tracked in OPEN_QUESTIONS.md.)
Pattern 5 — Dashboard provisioning
dashboardAPI doesn't operate on data — it generates Grafana dashboards. Any node can register with dashboardAPI via child.register; dashboardAPI then composes a dashboard JSON from the node's softwareType + measurements and POSTs to Grafana's HTTP API.
flowchart LR
subgraph EVOLV["EVOLV process nodes"]
ps[pumpingStation]:::pc
mgc[machineGroupControl]:::unit
rm[rotatingMachine]:::equip
end
subgraph UT["Utility"]
dash[dashboardAPI]:::util
end
grafana[(Grafana<br/>HTTP API)]:::ext
ps -. child.register .-> dash
mgc -. child.register .-> dash
rm -. child.register .-> dash
dash -->|POST /api/dashboards/db| grafana
classDef pc fill:#0c99d9,color:#fff
classDef unit fill:#50a8d9,color:#000
classDef equip fill:#86bbdd,color:#000
classDef util fill:#dddddd,color:#000
classDef ext fill:#fff2cc,color:#000
Notes:
dashboardAPIis the one node in the platform that doesn't extendBaseDomain(it's a passive HTTP bridge — see OPEN_QUESTIONS.md for the deferral decision).- The
metafield of dashboardAPI's outbound msg carries{nodeId, softwareType, uid, title}for correlating responses.
Putting it all together — example plant
A small WWTP combining all patterns:
flowchart TB
subgraph PC["Process Cell"]
ps1[pumpingStation<br/>inlet lift]:::pc
ps2[pumpingStation<br/>RAS pumping]:::pc
end
subgraph UN["Unit"]
mgc1[MGC inlet]:::unit
mgc2[MGC RAS]:::unit
vgc[VGC effluent split]:::unit
r1[reactor aerobic]:::unit
s1[settler]:::unit
mon[monster<br/>composite sampler]:::unit
end
subgraph EM["Equipment"]
rm1[pump A]:::equip
rm2[pump B]:::equip
rm3[RAS pump]:::equip
d1[diffuser]:::equip
v1[valve 1]:::equip
v2[valve 2]:::equip
end
ps1 --> mgc1
mgc1 --> rm1
mgc1 --> rm2
ps2 --> mgc2
mgc2 --> rm3
r1 ==stateChange==> s1
s1 -->|return pump| rm3
d1 -. OTR .-> r1
ps2 -. flow source .-> vgc
vgc --> v1
vgc --> v2
classDef pc fill:#0c99d9,color:#fff
classDef unit fill:#50a8d9,color:#000
classDef equip fill:#86bbdd,color:#000
This is the kind of diagram each wiki/Home.md per node should be able to fit into — every edge is reproducible from a configure() declaration.
Anti-patterns
- ❌
pumpingStation → valveGroupControlas 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 → reactorchild registration. Diffuser emits OTR via its emitter; reactor subscribes. Nochild.registerhandshake. - ❌
measurementparented underdashboardAPI. dashboardAPI accepts any node for Grafana provisioning, butmeasurementregisters with the process node it's monitoring, not with dashboardAPI.
Related pages
- Home — top-level node map
- Architecture — 3-tier code structure + generalFunctions API
- Topic-Conventions — what topics flow between nodes