For all 11 nodes with auto-gen markers: wiki/Home.md sections 5 (topic contract) and 9 (data model) regenerated via npm run wiki:all. New Unit column shows '<measure> (default <unit>)' for declared topics, '—' otherwise. Effect column now uses descriptor.description (P11.2 field) overriding the generic per-prefix fallback. For rotatingMachine + reactor: Phase 10 test rewrites — 3 + 8 files moved off private nodeClass internals (_attachInputHandler, _commands, _pendingExtras, _registerChild, _tick, etc.) to the public BaseNodeAdapter surface (node.handlers.input, node.source.*). +6 / +7 net new tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
11 KiB
settler
Reflects code as of
7bf464b· regenerated2026-05-11vianpm run wiki:allIf this banner is stale, the page may be out of date. Treat as informative, not authoritative.
1. What this node is
settler is an S88 Unit that models a secondary clarifier. It takes the upstream reactor's effluent stream, performs a 13-species TSS mass balance, and splits it into three Fluent envelopes: clarified effluent, surplus sludge, and return sludge. A downstream return pump (rotatingMachine child) draws the return-sludge flow.
2. Position in the platform
flowchart LR
upstream[reactor<br/>upstream<br/>Unit]:::unit
settler[settler<br/>Unit]:::unit
downstream[reactor<br/>downstream<br/>Unit]:::unit
return[rotatingMachine<br/>return pump<br/>Equipment]:::equip
tss[measurement<br/>type=quantity (tss)<br/>position=atequipment]:::ctrl
upstream -.stateChange.-> settler
settler -->|Fluent inlet=0,1,2| downstream
return -->|child.register downstream| settler
settler -.F_sr.-> return
tss -->|quantity (tss).measured.atequipment| settler
classDef unit fill:#50a8d9,color:#000
classDef equip fill:#86bbdd,color:#000
classDef ctrl fill:#a9daee,color:#000
S88 colours: Unit #50a8d9, Equipment #86bbdd, Control Module #a9daee. Source of truth: .claude/rules/node-red-flow-layout.md.
3. Capability matrix
| Capability | Status | Notes |
|---|---|---|
| TSS mass-balance split (3 streams) | ✅ | Effluent / surplus / return derived from F_in * Cs[12] / C_TS. |
| Particulate zeroing in effluent | ✅ | Species 7–12 set to 0 in effluent when F_s > 0. |
| Particulate concentration in sludge | ✅ | Species 7–12 scaled by F_in / F_s in surplus + return. |
| Return-pump flow draw | ✅ | F_sr = min(pump flow, F_s). Surplus = F_s − F_sr. |
| F_s clamp to F_in | ✅ | Prevents negative effluent when X_TS_in > C_TS. |
| Manual influent override | ✅ | data.influent lets ops supply { F, C } directly. |
| Multiple reactor upstreams | ❌ | Only one upstreamReactor slot; last registration wins. |
| Stateful FSM | ❌ | Stateless transform — recomputes on every push. |
4. Code map
flowchart TB
subgraph nodeRED["nodeClass.js — adapter (BaseNodeAdapter)"]
nc["buildDomainConfig()<br/>static DomainClass = Settler<br/>static commands"]
end
subgraph domain["specificClass.js — orchestrator (BaseDomain)"]
sc["Settler.configure()<br/>ChildRouter rules<br/>getEffluent — TSS split<br/>_connectReactor (manual listener)"]
end
subgraph commands["src/commands/"]
cmds["index.js + handlers.js<br/>data.influent + aliases"]
end
nc --> sc
nc --> cmds
| Module | Owns | Read first if you're changing… |
|---|---|---|
specificClass.js |
All domain logic: getEffluent split, reactor + machine + measurement wiring, getOutput, getStatusBadge. | Mass-balance math, child wiring, telemetry shape. |
commands/ |
Single command (data.influent) + aliases + payload validation. |
Manual-influent topic, new aliases. |
Settler is small enough (~140 LOC) that no concern-split was needed (per P6.6).
5. Topic contract
Auto-generated from
src/commands/index.js. Do NOT hand-edit between the markers. Re-runnpm run wiki:contract.
| Canonical topic | Aliases | Payload | Unit | Effect |
|---|---|---|---|---|
data.influent |
influent, setInfluent |
any |
— | Push the influent stream (payload: {F: flow m3/h, C: [concentrations mg/L]}). |
child.register |
registerChild |
string |
— | Register a child node (typically a measurement) with this settler. |
6. Child registration
flowchart LR
subgraph kids["accepted children (softwareType)"]
m["measurement"]:::ctrl
r["reactor<br/>upstream"]:::unit
mach["machine<br/>downstream"]:::equip
end
m -->|"<type>.measured.<position>"| h_m[_connectMeasurement]
r -.stateChange.-> h_r[_connectReactor<br/>manual listener]
mach -->|registered| h_mach[_connectMachine<br/>sets returnPump]
h_r --> pull[upstreamReactor.getEffluent]
pull --> emit[notifyOutputChanged]
classDef ctrl fill:#a9daee,color:#000
classDef unit fill:#50a8d9,color:#000
classDef equip fill:#86bbdd,color:#000
| softwareType | filter | wired to | side-effect |
|---|---|---|---|
measurement |
any | _connectMeasurement |
Re-emits on settler's measurements; quantity (tss) updates C_TS. |
reactor |
positionVsParent=upstream (warns otherwise) |
_connectReactor |
Stores as upstreamReactor; subscribes to its own emitter (NOT measurements.emitter) for 'stateChange'. |
machine |
positionVsParent=downstream |
_connectMachine |
Stores as returnPump; sets machine.upstreamSource = settler. |
6.1 Reactor ↔ settler wiring (the load-bearing bit)
The reactor pushes its stateChange event on reactor.emitter, not reactor.measurements.emitter. The standard router.onMeasurement path can't subscribe — so settler attaches the listener manually inside _connectReactor. On each fire, settler pulls the upstream effluent via reactor.getEffluent and copies it into this.F_in + this.Cs_in.
reactor.getEffluent historically returned either an array (3-stream) or a single envelope — the 2026-03-02 _connectReactor fix preserves both shapes:
const raw = this.upstreamReactor.getEffluent;
const effluent = Array.isArray(raw) ? raw[0] : raw;
this.F_in = effluent.payload.F;
this.Cs_in = effluent.payload.C;
this.notifyOutputChanged();
If you change the reactor's effluent shape, this is the line to update.
7. Lifecycle — what one stateChange does
sequenceDiagram
participant reactor as upstream reactor
participant settler as settler
participant pump as return pump child
participant downstream as downstream consumer
participant out as Port-0 output
reactor->>settler: emitter.emit('stateChange')
settler->>reactor: pull getEffluent
reactor-->>settler: { F, C[13] }
settler->>settler: F_in = F, Cs_in = C
settler->>pump: read measurements.flow.measured.atequipment
pump-->>settler: returnFlow
settler->>settler: getEffluent — split into 3 inlets
settler->>out: [Fluent inlet=0, Fluent inlet=1, Fluent inlet=2]
out->>downstream: 3 msgs on Port 0
The split runs lazily inside getEffluent: each call recomputes from current F_in, Cs_in, C_TS, and the pump's reported flow.measured.atequipment.
8. Data model — getOutput()
Port 0 carries the 3-envelope Fluent stream directly; Port 1 (this snapshot) is the scalar dashboard view.
| Key | Type | Unit | Sample |
|---|---|---|---|
C_TS |
number | — | 2500 |
F_eff |
number | — | 0 |
F_in |
number | — | 0 |
F_return |
number | — | 0 |
F_surplus |
number | — | 0 |
Concrete sample (typical operating point):
{
"F_in": 1000,
"C_TS": 2500,
"F_eff": 850.0,
"F_surplus": 50.0,
"F_return": 100.0
}
F_eff + F_surplus + F_return = F_in always holds (modulo float). Particulates concentrate by F_in / F_s in the surplus + return streams.
9. Configuration — editor form ↔ config keys
flowchart TB
subgraph editor["Node-RED editor form"]
f1[Name]
f2[Position vs parent]
f3[Logging level]
end
subgraph config["Domain config slice"]
c1[general.name]
c2[functionality.positionVsParent]
c3[general.logging.logLevel]
end
f1 --> c1
f2 --> c2
f3 --> c3
| Form field | Config key | Default | Range | Where used |
|---|---|---|---|---|
| Name | general.name |
Settler |
string | display + Port-1 topic |
| ID | general.id |
null |
nullable string | child registration key |
| Software type | functionality.softwareType |
settler |
string | parent-side router filter |
| Position vs parent | functionality.positionVsParent |
downstream |
enum: upstream / atEquipment / downstream |
parent-side routing |
| Logging level | general.logging.logLevel |
info |
enum | logger threshold |
Settler has no operational config of its own — all behaviour is driven by the runtime state (F_in, Cs_in, C_TS). Tune behaviour by feeding it different reactor effluents or C_TS measurements.
10. State chart
Skipped — settler is stateless. Each stateChange (or data.influent, or quantity (tss) update) recomputes the 3 Fluent streams from scratch.
11. Examples
| Tier | File | What it shows | Status |
|---|---|---|---|
| Basic | examples/basic.flow.json |
Inject data.influent, watch 3-stream split |
✅ in repo |
| Integration | examples/integration.flow.json |
reactor (upstream) + settler + return pump | ✅ in repo |
| Edge | examples/edge.flow.json |
F_s clamp + zero-influent fallback | ✅ in repo |
One screenshot per tier where helpful. PNG ≤ 200 KB under wiki/_partial-screenshots/settler/.
12. Debug recipes
| Symptom | First thing to check | Where to look |
|---|---|---|
F_eff negative or NaN |
C_TS zero or Cs_in[12] huge. F_s clamp should prevent — confirm clamp present. |
specificClass.js → getEffluent |
| Settler never updates after reactor changes | Reactor child not on 'upstream' position, or listener attached to wrong emitter. |
_connectReactor — listens on reactor.emitter, NOT measurements.emitter. |
| Return-sludge flow = 0 | returnPump.measurements.type('flow').variant('measured').position('atEquipment') empty. Wire a flow measurement on the pump. |
_connectMachine, pump measurement chain. |
| 3 Fluent envelopes not arriving downstream | payload.inlet selector on the downstream reactor mismatches (0=eff, 1=surplus, 2=return). |
downstream reactor's data.fluent handler. |
quantity (tss) updates don't change C_TS |
Measurement child's asset.type not quantity (tss) exactly. |
_updateMeasurement switch. |
13. When you would NOT use this node
- Use settler for secondary clarification downstream of a biological reactor. For primary sedimentation (raw sewage), the species-7-12 zeroing is wrong — model that as a separate process.
- Don't use settler as a generic mass-balance node — the 13-species ASM3 vector is hard-coded.
- Skip settler when the downstream reactor doesn't need a 3-stream split (e.g. single-tank SBR). A direct reactor → reactor wire is lighter.
14. Known limitations / current issues
| # | Issue | Tracked in |
|---|---|---|
| 1 | Only one upstreamReactor slot — multi-reactor settlers not supported (last registration wins). |
_connectReactor |
| 2 | TSS mass balance uses index 12 (X_TS) hard-coded — coupled tightly to ASM3 species ordering. |
getEffluent, _updateMeasurement |
| 3 | Settler depends on mathjs (~14 MB install) but only uses it transitively via reactor; no direct mathjs call in settler code. |
package.json |
| 4 | No flow-balance check at runtime — if particulate concentration drives F_s above F_in, the clamp masks an upstream bug rather than warning. | getEffluent |