Compare commits
1 Commits
b8247fc755
...
6953d6473e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6953d6473e |
@@ -16,7 +16,10 @@
|
|||||||
"author": "P.R. van der Wilt",
|
"author": "P.R. van der Wilt",
|
||||||
"main": "settler.js",
|
"main": "settler.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node --test test/basic/*.test.js test/integration/*.test.js test/edge/*.test.js"
|
"test": "node --test test/basic/*.test.js test/integration/*.test.js test/edge/*.test.js",
|
||||||
|
"wiki:contract": "node ../generalFunctions/scripts/wikiGen.js contract ./src/commands/index.js --write ./wiki/Home.md",
|
||||||
|
"wiki:datamodel": "node ../generalFunctions/scripts/wikiGen.js datamodel ./src/specificClass.js --write ./wiki/Home.md",
|
||||||
|
"wiki:all": "npm run wiki:contract && npm run wiki:datamodel"
|
||||||
},
|
},
|
||||||
"node-red": {
|
"node-red": {
|
||||||
"nodes": {
|
"nodes": {
|
||||||
|
|||||||
241
wiki/Home.md
Normal file
241
wiki/Home.md
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
# settler
|
||||||
|
|
||||||
|
> **Reflects code as of `7bf464b` · regenerated `2026-05-11` via `npm run wiki:all`**
|
||||||
|
> If 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
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
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
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
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-run `npm run wiki:contract`.
|
||||||
|
|
||||||
|
<!-- BEGIN AUTOGEN: topic-contract -->
|
||||||
|
|
||||||
|
| Canonical topic | Aliases | Payload | Effect |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `data.influent` | `influent`, `setInfluent` | `any` | Pushes a value into the node's measurement stream. |
|
||||||
|
|
||||||
|
<!-- END AUTOGEN: topic-contract -->
|
||||||
|
|
||||||
|
## 6. Child registration
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
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:
|
||||||
|
|
||||||
|
```js
|
||||||
|
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
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
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.
|
||||||
|
|
||||||
|
<!-- BEGIN AUTOGEN: data-model -->
|
||||||
|
|
||||||
|
| Key | Type | Unit | Sample |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `C_TS` | number | — | `2500` |
|
||||||
|
| `F_eff` | number | — | `0` |
|
||||||
|
| `F_in` | number | — | `0` |
|
||||||
|
| `F_return` | number | — | `0` |
|
||||||
|
| `F_surplus` | number | — | `0` |
|
||||||
|
|
||||||
|
<!-- END AUTOGEN: data-model -->
|
||||||
|
|
||||||
|
**Concrete sample** (typical operating point):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
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` |
|
||||||
Reference in New Issue
Block a user