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>
249 lines
9.3 KiB
Markdown
249 lines
9.3 KiB
Markdown
# Topology Patterns
|
|
|
|
> **Reflects code as of `9ab9f6b` · regenerated `2026-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.
|
|
|
|
```mermaid
|
|
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:**
|
|
- `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.
|
|
|
|
**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.
|
|
|
|
## 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.
|
|
|
|
```mermaid
|
|
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()` 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.
|
|
|
|
**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.
|
|
|
|
## 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.
|
|
|
|
```mermaid
|
|
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.
|
|
|
|
```mermaid
|
|
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.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.)
|
|
|
|
## 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.
|
|
|
|
```mermaid
|
|
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:**
|
|
- `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.
|
|
|
|
## Putting it all together — example plant
|
|
|
|
A small WWTP combining all patterns:
|
|
|
|
```mermaid
|
|
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 → 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.
|
|
|
|
## 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
|