diff --git a/wiki/Home.md b/wiki/Home.md index 54d75ec..4c61256 100644 --- a/wiki/Home.md +++ b/wiki/Home.md @@ -1,251 +1,134 @@ # settler -> **Reflects code as of `94b6616` · 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. +![code-ref](https://img.shields.io/badge/code--ref-a3583a3-blue) ![s88](https://img.shields.io/badge/S88-Unit-50a8d9) ![status](https://img.shields.io/badge/status-stub--level-lightgrey) -## 1. What this node is +A `settler` models a secondary clarifier — the sludge-separation stage that sits downstream of a biological reactor. It receives the upstream reactor's effluent stream, performs a 13-species TSS mass balance, and splits the result into three Fluent envelopes: clarified effluent, surplus sludge, and return sludge. A downstream return pump (a `rotatingMachine` registered as `machine` / `downstream`) draws the return-sludge flow. -**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. +> [!NOTE] +> Pending full node review (2026-05). Content reflects `CONTRACT.md` and current source only. The shipped `examples/` folder ships stubs only — production-grade flows are still TODO. Treat this page as informative, not authoritative. -## 2. Position in the platform +--- + +## At a glance + +| Thing | Value | +|:---|:---| +| What it represents | Secondary clarifier / sludge settler — the gravity-separation stage between a biological reactor and its downstream sludge handling | +| S88 level | Unit | +| Use it when | You have a reactor whose effluent must be split into clarified water + return / surplus sludge by a TSS mass balance | +| Don't use it for | Primary sedimentation (species 7–12 zeroing is wrong), generic mass-balance transforms (the 13-species ASM3 vector is hard-coded), single-tank SBRs that don't need a 3-stream split | +| Children it accepts | `measurement` (any, but `quantity (tss)` is the only one that mutates state), `reactor` (upstream), `machine` (downstream — the return pump) | +| Parents it talks to | Typically a downstream `reactor` — the three Fluent streams are routed by `payload.inlet` | + +--- + +## How it fits ```mermaid flowchart LR upstream[reactor
upstream
Unit]:::unit settler[settler
Unit]:::unit downstream[reactor
downstream
Unit]:::unit - return[rotatingMachine
return pump
Equipment]:::equip - tss[measurement
type=quantity (tss)
position=atequipment]:::ctrl + return_pump[rotatingMachine
return pump
Equipment]:::equip + tss[measurement
quantity tss
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 + settler -->|Fluent inlet=0 effluent| downstream + settler -->|Fluent inlet=1 surplus| downstream + settler -->|Fluent inlet=2 return| return_pump + return_pump -->|child.register downstream| settler + 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`. +S88 colours are anchored in `.claude/rules/node-red-flow-layout.md`. The settler editor colour is currently `#e4a363` (orange) — tracked as drift in §16 of that rule; diagrams in this wiki use the correct Unit blue (`#50a8d9`). -## 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. | +## Try it — 1-minute demo -## 4. Code map +> [!IMPORTANT] +> The shipped examples (`examples/basic.flow.json`, `integration.flow.json`, `edge.flow.json`) are skeleton stubs — they create a settler node and a debug tap, but do not exercise the reactor → settler → pump chain. A proper Tier-1 / Tier-2 / Tier-3 example set is on the TODO list; until then this section walks the minimum stimulus. -```mermaid -flowchart TB - subgraph nodeRED["nodeClass.js — adapter (BaseNodeAdapter)"] - nc["buildDomainConfig()
static DomainClass = Settler
static commands"] - end - subgraph domain["specificClass.js — orchestrator (BaseDomain)"] - sc["Settler.configure()
ChildRouter rules
getEffluent — TSS split
_connectReactor (manual listener)"] - end - subgraph commands["src/commands/"] - cmds["index.js + handlers.js
data.influent + aliases"] - end - nc --> sc - nc --> cmds +Import the basic stub, deploy, then drive influent manually: + +```bash +curl -X POST -H 'Content-Type: application/json' \ + --data @nodes/settler/examples/basic.flow.json \ + http://localhost:1880/flow ``` -| 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. | +After deploy, send one inject: -Settler is small enough (~140 LOC) that no concern-split was needed (per P6.6). +| Topic | Payload | What it does | +|:---|:---|:---| +| `data.influent` | `{ "F": 1000, "C": [0,0,0,0,0,0,0,0,0,0,0,0,3000] }` | Pushes 1000 m³/h influent with 3000 mg/L total solids (index 12 = `X_TS`). Three Fluent envelopes appear on Port 0 immediately. | -## 5. Topic contract +> [!NOTE] +> Pending full node review (2026-05). Real flows (Tier-1 inject only, Tier-2 reactor + settler + pump, Tier-3 dashboard) are not yet shipped. See [Reference — Examples](Reference-Examples). -> **Auto-generated** from `src/commands/index.js`. Do NOT hand-edit between the markers. Re-run `npm run wiki:contract`. +--- - +## The two things you'll send -| 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. | +| Topic | Aliases | Payload | What it does | +|:---|:---|:---|:---| +| `data.influent` | `influent`, `setInfluent` | `{F: number, C: number[13]}` — either field optional | Override the influent stream directly. Triggers a recompute of all three Fluent envelopes. | +| `child.register` | `registerChild` | `string` (child node id) | Register a `measurement`, `reactor`, or `machine` child. Port 2 wiring does this automatically in normal flows. | - +That is the entire input contract. Settler has no FSM, no setpoint, no startup sequence — it is a stateless transform on top of whatever the upstream reactor + measurement children push into it. -## 6. Child registration +--- -```mermaid -flowchart LR - subgraph kids["accepted children (softwareType)"] - m["measurement"]:::ctrl - r["reactor
upstream"]:::unit - mach["machine
downstream"]:::equip - end - m -->|"<type>.measured.<position>"| h_m[_connectMeasurement] - r -.stateChange.-> h_r[_connectReactor
manual listener] - mach -->|registered| h_mach[_connectMachine
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 -``` +## What you'll see come out -| 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. - - - -| 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): +Sample Port 0 messages (one push, three envelopes): ```json -{ - "F_in": 1000, - "C_TS": 2500, - "F_eff": 850.0, - "F_surplus": 50.0, - "F_return": 100.0 -} +[ + { "topic": "Fluent", "payload": { "inlet": 0, "F": 850.0, "C": [/*7-12 zeroed*/] }, "timestamp": 1715000000000 }, + { "topic": "Fluent", "payload": { "inlet": 1, "F": 50.0, "C": [/*7-12 concentrated*/] }, "timestamp": 1715000000000 }, + { "topic": "Fluent", "payload": { "inlet": 2, "F": 100.0, "C": [/*7-12 concentrated*/] }, "timestamp": 1715000000000 } +] ``` -`F_eff + F_surplus + F_return = F_in` always holds (modulo float). Particulates concentrate by `F_in / F_s` in the surplus + return streams. +| `payload.inlet` | Meaning | Particulate species (indices 7–12) | +|:---:|:---|:---| +| `0` | Clarified effluent | zeroed when `F_s > 0` | +| `1` | Surplus sludge (the fraction the return pump does not draw) | concentrated by `F_in / F_s` | +| `2` | Return sludge (drawn by the downstream return pump, capped at `F_s`) | concentrated by `F_in / F_s` | -## 9. Configuration — editor form ↔ config keys +Mass balance invariant: `F_eff + F_surplus + F_return = F_in` (modulo float). -```mermaid -flowchart TB - subgraph editor["Node-RED editor form"] - f1[Name] - f2[Process Output Format] - f3[Database Output Format] - f4[Logging level] - f5[Position vs parent] - end - subgraph config["Domain config / nodeClass"] - c1[general.name] - c2[processOutputFormat → nodeClass] - c3[dbaseOutputFormat → nodeClass] - c4[general.logging.logLevel] - c5[functionality.positionVsParent] - end - f1 --> c1 - f2 --> c2 - f3 --> c3 - f4 --> c4 - f5 --> c5 -``` +Port 1 (InfluxDB) is the scalar dashboard view — see [Reference — Contracts](Reference-Contracts#data-model--getoutput-shape). Port 2 carries the one-shot `child.register` upward at startup. -| Form field | Config key | Default | Range | Where used | -|---|---|---|---|---| -| Name | `general.name` | `Settler` | string | display + Port-1 topic | -| Process Output Format | `processOutputFormat` (nodeClass) | `process` | `process` / `json` / `csv` | Port-0 serialisation | -| Database Output Format | `dbaseOutputFormat` (nodeClass) | `influxdb` | `influxdb` / `json` / `csv` | Port-1 serialisation | -| Logging level | `general.logging.logLevel` | `info` | `debug` / `info` / `warn` / `error` | logger threshold | -| Position vs parent | `functionality.positionVsParent` | `downstream` | `upstream` / `atEquipment` / `downstream` | parent-side routing | -| Software type | `functionality.softwareType` | `settler` | string | parent-side router filter | -| ID | `general.id` | `null` | nullable string | child registration key | +--- -Settler has no operational process config of its own — all behaviour is driven by runtime state (`F_in`, `Cs_in`, `C_TS`). Tune behaviour by feeding it different reactor effluents or `C_TS` measurements. +## Capability matrix -## 10. State chart +| Capability | Status | Notes | +|:---|:---:|:---| +| TSS mass-balance split (3 streams) | yes | `F_s = min(F_in * Cs[12] / C_TS, F_in)` — clamped to prevent negative effluent. | +| Particulate zeroing in effluent | yes | Species 7–12 set to 0 in `inlet=0` when `F_s > 0`. | +| Particulate concentration in sludge | yes | Species 7–12 scaled by `F_in / F_s` in `inlet=1` + `inlet=2`. | +| Return-pump flow draw | yes | `F_sr = min(pump flow at equipment, F_s)`. Surplus = `F_s - F_sr`. | +| `F_s` clamp to `F_in` | yes | Prevents negative effluent when `X_TS_in > C_TS`. | +| Manual influent override | yes | `data.influent` lets ops supply `{F, C}` directly. | +| Multiple reactor upstreams | no | Only one `upstreamReactor` slot; last registration wins. | +| Stateful FSM | no | Stateless transform — recomputes on every trigger. | +| Curve loading / drift / sequence-abort | n/a | Not applicable to a passive split. | -Not applicable — settler is stateless. There is no FSM. Every trigger (`stateChange` from the reactor, `data.influent`, or a `quantity (tss)` update) causes a fresh recompute of the 3 Fluent streams from the current runtime state and the split immediately re-emits. +--- -## 11. Examples +## Need more? -| 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 | +| Page | What you'll find | +|:---|:---| +| [Reference — Contracts](Reference-Contracts) | Topic registry, config schema, child registration filters | +| [Reference — Architecture](Reference-Architecture) | Three-tier code map, reactor ↔ settler wiring (the load-bearing bit), lifecycle, output ports | +| [Reference — Examples](Reference-Examples) | Shipped example flows (currently stubs) + the TODO list for production-grade demos | +| [Reference — Limitations](Reference-Limitations) | When not to use, known limitations, open questions | -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` | -| 5 | Editor colour is `#e4a363` (orange) in `settler.html` but S88 Unit level requires `#50a8d9` (blue). Diagrams in this wiki use the correct `#50a8d9`. Colour cleanup tracked in `.claude/rules/node-red-flow-layout.md` §16. | `settler.html` | +[EVOLV master wiki](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Home) · [Topology Patterns](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topology-Patterns) · [Topic Conventions](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions) diff --git a/wiki/Reference-Architecture.md b/wiki/Reference-Architecture.md new file mode 100644 index 0000000..0b5cb54 --- /dev/null +++ b/wiki/Reference-Architecture.md @@ -0,0 +1,291 @@ +# Reference — Architecture + +![code-ref](https://img.shields.io/badge/code--ref-a3583a3-blue) + +> [!NOTE] +> Pending full node review (2026-05). Content reflects `CONTRACT.md` and current source only. + +Code structure for `settler`: the three-tier sandwich, the `src/` layout, the reactor ↔ settler wiring (the load-bearing bit), the lifecycle, and the output-port pipeline. For an intuitive overview, return to [Home](Home). + +--- + +## Three-tier code layout + +``` +nodes/settler/ +| ++-- settler.js entry: RED.nodes.registerType('settler', ...) + /settler/menu.js admin route ++-- settler.html editor form (Name, output formats, logger, position) — currently colour-drifted to #e4a363 +| ++-- src/ +| nodeClass.js extends BaseNodeAdapter — domain wiring + Port 0/1 emit (~30 LOC) +| specificClass.js extends BaseDomain — TSS split + child wiring (~140 LOC) +| | +| +-- commands/ +| index.js 2 topic descriptors: data.influent (+aliases), child.register +| handlers.js pure handler functions +``` + +Settler is small enough (~140 LOC of domain code) that no concern-split was needed; per the platform's MODULE_SPLIT contract a node only fans out into `src//` modules when complexity demands it. + +### Tier responsibilities + +| Tier | File | What it owns | Touches `RED.*` | +|:---|:---|:---|:---:| +| entry | `settler.js` | Type registration + `/settler/menu.js` admin endpoint (proxies reactor's MenuManager). | yes | +| nodeClass | `src/nodeClass.js` | `_emitOutputs()` only — calls `source.getEffluent` for the 3-msg Port-0 array and `source.getOutput()` for Port-1 InfluxDB telemetry. `tickInterval = null` (event-driven, no periodic loop). `statusInterval = 1000` for the badge. | yes | +| specificClass | `src/specificClass.js` | `Settler.configure()` wires the `ChildRouter` for `measurement` / `reactor` / `machine` children. `getEffluent` is the mass-balance core. `getOutput` is the scalar Port-1 view. `getStatusBadge` renders the editor badge. | no | + +`specificClass` does the work directly; there is no orchestration / concern layer between it and the math. + +> [!NOTE] +> Pending full node review (2026-05). The router-based registration uses `this.router.onRegister(...)` — verify against the latest BaseDomain contract when next touched. + +--- + +## FSM + +Not applicable. Settler is **stateless**. There is no FSM, no `state.*` member, no `executeSequence`, no movement / setpoint. Every trigger (`stateChange` from the upstream reactor, `data.influent` from an operator, or a `quantity (tss)` update from a measurement child) causes a fresh recompute of the three Fluent envelopes from the current runtime state, and the split immediately re-emits. + +The status badge has two cosmetic branches only: + +| Condition | Badge | +|:---|:---| +| `F_in <= 0` | `statusBadge.idle('no influent')` | +| else | green dot, label `F_in= eff= surplus=` | + +There are no "protected states", no abort tokens, no allow-lists. All of that belongs to FSM-bearing nodes (`rotatingMachine`, `pumpingStation`, `machineGroupControl`, …). + +--- + +## Reactor ↔ settler wiring — the load-bearing bit + +This is the one piece of settler that needs care, because the reactor uses a different event channel than every other child type in EVOLV. + +```mermaid +flowchart TB + reactor[upstream reactor]:::unit + rEmit{reactor.emitter} + rMeas{reactor.measurements.emitter} + settler[settler._connectReactor]:::unit + pull[upstreamReactor.getEffluent] + apply[F_in = ...
Cs_in = ...] + notify[notifyOutputChanged()] + + reactor --> rEmit + reactor --> rMeas + rEmit -- stateChange --> settler + rMeas -. NOT used .- settler + settler --> pull + pull --> apply + apply --> notify + classDef unit fill:#50a8d9,color:#000 +``` + +Mechanism: + +1. The reactor pushes its `stateChange` event on `reactor.emitter` — **not** on `reactor.measurements.emitter`. The standard `router.onMeasurement` subscription path therefore can't see it. +2. `_connectReactor(reactorChild)` attaches the listener manually: + + ```js + reactorChild.emitter.on('stateChange', () => { + 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(); + }); + ``` + +3. `reactor.getEffluent` historically returned either an **array** (a 3-stream Fluent envelope, same shape settler itself emits) or a **single envelope** — the 2026-03-02 `_connectReactor` fix preserves both shapes via `Array.isArray(raw) ? raw[0] : raw`. If you change the reactor's effluent shape, this is the line to update. +4. Position check: settler warns `Reactor children of settlers should be upstream.` if `positionVsParent !== 'upstream'`, but still stores the reactor. The wiring is not blocked — it just becomes the operator's problem to confirm intent. + +> [!NOTE] +> Pending full node review (2026-05). The settler-side pull-not-push semantics rely on `reactor.getEffluent` being a property getter (no arguments). Future reactor refactors that turn this into a parameterised method will need a coordinated change here. + +--- + +## Return-pump wiring + +`_connectMachine(machineChild)` is the second non-trivial registration path: + +| Condition | Effect | +|:---|:---| +| `positionVsParent === 'downstream'` | Store as `this.returnPump`. Set `machineChild.upstreamSource = this` so the pump's own flow predictor can use settler's `inlet=2` envelope as its suction-side Fluent. | +| else | Warn `Failed to register machine child.`, no storage. | + +At each call to `getEffluent`, settler reads the pump's current measured flow: + +```js +F_sr = Math.min( + this.returnPump.measurements.type('flow').variant('measured').position(POSITIONS.AT_EQUIPMENT).getCurrentValue(), + F_s, +); +``` + +If no return pump is registered or its flow measurement is zero, `F_sr = 0` — in that case all separated sludge becomes surplus (`inlet=1`) and the return stream (`inlet=2`) is zero-flow. + +--- + +## Measurement-child wiring + +`_connectMeasurement(measurementChild)` is generic: it subscribes to `.measured.` on the child's `measurements.emitter` and: + +1. Re-emits the value on settler's own `this.measurements` container (lets settler's own parent subscribe). +2. Calls `_updateMeasurement(type, value, position, eventData)`. + +`_updateMeasurement` currently recognises only one type: + +| `measurementType` | Side-effect | +|:---|:---| +| `quantity (tss)` | Set `this.C_TS = value`. Trigger `notifyOutputChanged()`. | +| anything else | Log an `error` (`Type '' not recognized for measured update.`) — the re-emit still happened, just no settler-side state change. | + +The `quantity (tss)` value is the **settler's own setpoint** for the target return-sludge concentration. The default is `2500` mg/L. Higher `C_TS` → less return + surplus, more effluent. Lower `C_TS` → more return + surplus. + +--- + +## Mass-balance math (`getEffluent`) + +```mermaid +flowchart TB + F_in[F_in m³/h]:::input + Cs_in[Cs_in array 13
mg/L per species]:::input + C_TS[C_TS
target return mg/L]:::input + pumpFlow[returnPump.flow.measured.atequipment]:::input + + F_in --> Fs[F_s = min(F_in × Cs_in[12] / C_TS, F_in)] + Cs_in --> Fs + C_TS --> Fs + Fs --> F_eff[F_eff = F_in − F_s] + Fs --> F_sr[F_sr = min(pumpFlow, F_s)] + pumpFlow --> F_sr + F_sr --> F_so[F_so = F_s − F_sr] + + Cs_in --> CsEff[Cs_eff: copy then zero 7..12 if F_s > 0] + Cs_in --> CsS[Cs_s: copy then scale 7..12 by F_in / F_s if F_s > 0] + + F_eff --> envEff[envelope inlet=0] + CsEff --> envEff + F_so --> envSur[envelope inlet=1] + CsS --> envSur + F_sr --> envRet[envelope inlet=2] + CsS --> envRet + classDef input fill:#a9daee,color:#000 +``` + +Key facts: + +- `F_s` is the **total separated sludge stream**. Mass-balance derivation: at steady state, `F_in * Cs_in[12] = F_s * C_TS` → `F_s = F_in * Cs_in[12] / C_TS`. +- The clamp `min(..., F_in)` prevents `F_eff` going negative when `Cs_in[12] > C_TS` (i.e. the influent is denser than the target sludge concentration). Sub-rosa: the clamp masks the **upstream** problem; settler does not warn when the clamp fires — see [Reference — Limitations](Reference-Limitations#no-flow-balance-warning). +- Species indices 7–12 are the ASM3 particulate species (`X_*`). Index 12 specifically is `X_TS` — the lumped total-suspended-solids surrogate the split is keyed off. +- Soluble species 0–6 pass through unchanged in all three streams. +- All three envelopes share a single `timestamp = Date.now()` — downstream consumers can rely on them being a coherent triple. + +--- + +## Lifecycle — what one trigger does + +```mermaid +sequenceDiagram + autonumber + participant reactor as upstream reactor + participant settler as settler (specificClass) + participant nc as nodeClass + participant pump as return pump child + participant out as Port 0 / 1 + + reactor->>settler: emitter.emit('stateChange') + settler->>reactor: read getEffluent + reactor-->>settler: {F, C[13]} (or [{...}]) + settler->>settler: F_in = F · Cs_in = C + settler->>settler: notifyOutputChanged() + nc->>settler: read getEffluent (recompute) + settler->>pump: read measurements.flow.measured.atequipment + pump-->>settler: returnFlow (m³/h) + settler->>settler: getEffluent — split into 3 envelopes + settler-->>nc: [Fluent inlet=0, inlet=1, inlet=2] + nc->>settler: read getOutput() + settler-->>nc: {F_in, C_TS, F_eff, F_surplus, F_return, ...measurements} + nc->>out: send([fluent, influxMsg, null]) +``` + +The three triggers that route through this lifecycle are: + +| Trigger | Origin | Path | +|:---|:---|:---| +| Reactor `stateChange` | `reactor.emitter.emit('stateChange')` | `_connectReactor` listener → pull `getEffluent` → copy → `notifyOutputChanged` | +| Operator `data.influent` | Inbound `msg.topic` | `commands/handlers.js#dataInfluent` → mutate `F_in` / `Cs_in` → `notifyOutputChanged` | +| Measurement `quantity (tss)` | `measurementChild.measurements.emitter` | `_connectMeasurement` re-emit + `_updateMeasurement` → mutate `C_TS` → `notifyOutputChanged` | + +`notifyOutputChanged` is BaseDomain's standard `output-changed` event. `BaseNodeAdapter` listens, calls `_emitOutputs()`, which produces the Port 0 / Port 1 messages. + +--- + +## Output ports + +| Port | Carries | Sample shape | +|:---|:---|:---| +| 0 (process) | **Array of three Node-RED messages**, each `{topic: 'Fluent', payload: {inlet, F, C}, timestamp}`. Re-emitted on every recompute. | See [Home — What you'll see come out](Home#what-youll-see-come-out). | +| 1 (telemetry) | Single InfluxDB line-protocol payload built by `outputUtils.formatMsg(getOutput(), cfg, 'influxdb')`. Delta-compressed: only changed fields shipped. | `settler,id=settler_a F_in=1000,F_eff=850,F_surplus=50,F_return=100,C_TS=2500,...` | +| 2 (register / control) | `null` from `_emitOutputs`. The one-shot `child.register` upward at startup goes through the BaseNodeAdapter init path, not `_emitOutputs`. | `{topic: 'child.register', payload: , positionVsParent, distance}` (init only) | + +Port-1 key shape from `getOutput()`: + +| Key | Type | Source | Notes | +|:---|:---|:---|:---| +| `F_in` | number | `host.F_in` | Influent flow (m³/h). | +| `C_TS` | number | `host.C_TS` | Target return-sludge concentration (mg/L). Default 2500. | +| `F_eff` | number | `streams[0].payload.F` | Clarified effluent flow. | +| `F_surplus` | number | `streams[1].payload.F` | Surplus sludge flow. | +| `F_return` | number | `streams[2].payload.F` | Return sludge flow. | +| `...` | varies | `measurements.getFlattenedOutput()` | Flattened snapshot of every measurement settler has seen (re-emitted from children). | + +See [EVOLV — Telemetry](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Telemetry) for the full InfluxDB layout. + +> [!NOTE] +> Pending full node review (2026-05). The `getOutput()` return shape has not been audited against an `_output-manifest.md` (the output-coverage rule). TODO: add the manifest and `test/basic/output-*.test.js` coverage in both populated and degraded states (no influent / no pump / no TSS measurement). + +--- + +## Event sources + +| Source | Where it fires | What it triggers | +|:---|:---|:---| +| `reactor.emitter` `'stateChange'` | Upstream reactor on every internal state advance | `_connectReactor` listener → pull `getEffluent` → recompute | +| `measurementChild.measurements.emitter` `'.measured.'` | Any registered measurement child on a new sample | `_connectMeasurement` re-emit + `_updateMeasurement` switch | +| Inbound `msg.topic = data.influent` | Node-RED input wire | `commands/handlers.js#dataInfluent` | +| Inbound `msg.topic = child.register` | Node-RED input wire (rare; usually Port 2 wiring) | `commands/handlers.js#childRegister` | +| `BaseDomain` `'output-changed'` | `notifyOutputChanged()` in domain | `nodeClass._emitOutputs()` | +| `setInterval(statusInterval = 1000)` | `BaseNodeAdapter` | Re-render the status badge | + +No per-second tick on the domain itself. `tickInterval = null` is explicit in `nodeClass`. + +--- + +## Where to start reading + +| If you're changing… | Read first | +|:---|:---| +| The TSS mass-balance math | `src/specificClass.js#getEffluent` (lines 37–62) | +| Reactor ↔ settler wiring (stateChange listener, both-shape envelope handling) | `src/specificClass.js#_connectReactor` | +| Return-pump wiring (the `machine` / `downstream` registration) | `src/specificClass.js#_connectMachine` | +| Measurement re-emit + `C_TS` setpoint | `src/specificClass.js#_connectMeasurement` + `#_updateMeasurement` | +| Operator-side influent override | `src/commands/handlers.js#dataInfluent` | +| Port 0 (3-msg array) + Port 1 (InfluxDB) emit pipeline | `src/nodeClass.js#_emitOutputs` | +| Status-badge text | `src/specificClass.js#getStatusBadge` | +| Editor form / colour drift | `settler.html` (currently `color: '#e4a363'`; should be `#50a8d9`) | + +--- + +## Related pages + +| Page | Why | +|:---|:---| +| [Home](Home) | Intuitive overview | +| [Reference — Contracts](Reference-Contracts) | Topic + config + child filters | +| [Reference — Examples](Reference-Examples) | Shipped flows + debug recipes | +| [Reference — Limitations](Reference-Limitations) | Known issues and open questions | +| [reactor wiki](https://gitea.wbd-rd.nl/RnD/reactor/wiki/Home) | The upstream parent — emits `stateChange` and exposes `getEffluent` | +| [rotatingMachine wiki](https://gitea.wbd-rd.nl/RnD/rotatingMachine/wiki/Home) | The return-pump child — consumes `inlet=2` via `upstreamSource` | +| [EVOLV — Architecture](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Architecture) | Platform-wide three-tier pattern | diff --git a/wiki/Reference-Contracts.md b/wiki/Reference-Contracts.md new file mode 100644 index 0000000..aebdd6d --- /dev/null +++ b/wiki/Reference-Contracts.md @@ -0,0 +1,176 @@ +# Reference — Contracts + +![code-ref](https://img.shields.io/badge/code--ref-a3583a3-blue) + +> [!NOTE] +> Pending full node review (2026-05). Content reflects `CONTRACT.md`, `src/commands/index.js`, and `generalFunctions/src/configs/settler.json` only. + +Full topic contract, configuration schema, and child-registration filters for `settler`. Source of truth: `src/commands/index.js`, `src/specificClass.js` `configure()`, and the schema at `generalFunctions/src/configs/settler.json`. For an intuitive overview, return to the [Home](Home). + +--- + +## Topic contract + +The registry lives in `src/commands/index.js`. Each descriptor maps a canonical `msg.topic` to its handler; aliases emit a one-time deprecation warning the first time they fire. + + + +| Canonical topic | Aliases | Payload | Unit | Effect | +|:---|:---|:---|:---|:---| +| `data.influent` | `influent`, `setInfluent` | `{F: number, C: number[13]}` — either field optional | `F` in m³/h, `C[*]` in mg/L | Replaces influent flow and/or the 13-species concentration vector on the domain (`source.F_in`, `source.Cs_in`). Triggers `notifyOutputChanged`, which re-emits the 3-stream Fluent envelope on Port 0. | +| `child.register` | `registerChild` | `string` (child node id) | — | Register a child node (measurement / reactor / machine) with this settler. Port 2 wiring does this automatically in normal flows; the explicit handler exists because `BaseNodeAdapter` does not have an implicit registration path. | + + + +> [!NOTE] +> Pending full node review (2026-05). The autogen markers above will be populated by a future `npm run wiki:contract` tool. Until then the table is hand-maintained against `src/commands/index.js`. + +### Mode / source / action allow-lists + +**Not applicable.** Settler has no operational mode, no source allow-list, no action allow-list. Both topics are accepted unconditionally; payload-shape validation lives in the handler itself. + +The `data.influent` handler validates: + +```js +if (!p || typeof p !== 'object' || Array.isArray(p)) { + log?.warn?.(`data.influent expects an object {F, C}; got ${typeof p}`); + return; +} +if (typeof p.F === 'number' && Number.isFinite(p.F)) source.F_in = p.F; +if (Array.isArray(p.C)) source.Cs_in = [...p.C]; +``` + +Non-finite or non-numeric `F` is silently ignored. Non-array `C` is silently ignored. Either field may be omitted to update only the other. Negative `F` is **not** rejected — the downstream `getEffluent` math will produce nonsense but the node will not throw. + +--- + +## Data model — `getOutput()` shape + +Composed each tick by `src/specificClass.js` `getOutput()`. Port 0 carries the 3-message Fluent stream **directly** (not via `getOutput`); Port 1 (this snapshot) is the scalar dashboard view. + + + +### Scalar keys + +| Key | Type | Unit | Source | Notes | +|:---|:---|:---|:---|:---| +| `F_in` | number | m³/h | `host.F_in` | Influent flow. Default 0. | +| `C_TS` | number | mg/L | `host.C_TS` | Target return-sludge concentration. Default 2500. Updated by `quantity (tss)` measurement child. | +| `F_eff` | number | m³/h | `streams[0].payload.F` | Clarified effluent flow. | +| `F_surplus` | number | m³/h | `streams[1].payload.F` | Surplus sludge flow (`F_s - F_sr`). | +| `F_return` | number | m³/h | `streams[2].payload.F` | Return sludge flow (`min(pumpFlow, F_s)`). | + +### Per-measurement keys + +For every `(type, variant, position)` stored in `this.measurements` MeasurementContainer, the flattened output emits: + +``` +... +``` + +Position labels are normalised to lowercase. The trailing `` is the registering measurement child's id. Settler does not write its own measurements directly — every key in this group came from a registered child (the `_connectMeasurement` re-emit). + + + +### Status badge + +`getStatusBadge()` in `src/specificClass.js`: + +| Condition | Badge | +|:---|:---| +| `F_in <= 0` | `statusBadge.idle('no influent')` (grey ring) | +| else | green dot, label `F_in= eff= surplus=` (m³/h, 2 dp) | + +No state-symbol enumeration — settler has no FSM. + +--- + +## Configuration schema — editor form to config keys + +Source of truth: `generalFunctions/src/configs/settler.json` plus `settler.html`. + +### General (`config.general`) + +| Form field | Config key | Default | Notes | +|:---|:---|:---|:---| +| Name | `general.name` | `"Settler"` | Human-readable name. | +| (auto-assigned) | `general.id` | `null` | Node-RED node id. | +| Default unit | `general.unit` | `null` | Default measurement unit. Currently unused by settler — child measurements carry their own units. | +| Enable logging | `general.logging.enabled` | `true` | Master switch. | +| Log level | `general.logging.logLevel` | `info` | `debug` / `info` / `warn` / `error`. | + +### Functionality (`config.functionality`) + +| Form field | Config key | Default | Notes | +|:---|:---|:---|:---| +| (hidden) | `functionality.softwareType` | `settler` | Constant. Used by the parent's router as a registration filter. | +| (hidden) | `functionality.role` | `Secondary settler for sludge separation` | Documentation string. | +| Position vs parent | `functionality.positionVsParent` | `downstream` | One of `upstream` / `atEquipment` / `downstream`. Settler typically registers as `downstream` against an upstream reactor. | + +### Node-RED-side (editor form, not domain config) + +| Form field | Stored on | Default | Notes | +|:---|:---|:---|:---| +| Process Output Format | `nodeClass.processOutputFormat` | `process` | `process` / `json` / `csv`. Port-0 serialisation. | +| Database Output Format | `nodeClass.dbaseOutputFormat` | `influxdb` | `influxdb` / `json` / `csv`. Port-1 serialisation. | + +`buildDomainConfig()` returns `{}` — settler does not push any editor-derived values into the domain at start-up. All operational state is runtime (`F_in`, `Cs_in`, `C_TS`). + +> [!NOTE] +> Pending full node review (2026-05). The editor form (`settler.html`) is currently colour-drifted to `#e4a363` (orange); should be `#50a8d9` (Unit blue) per `.claude/rules/node-red-flow-layout.md` §16. + +### Unit policy + +| Quantity | Canonical (internal) | Output (Port 1) | Notes | +|:---|:---|:---|:---| +| Flow (`F_in`, `F_eff`, `F_surplus`, `F_return`) | m³/h | m³/h | No conversion — settler is unit-agnostic above the storage layer. | +| Concentration (`C_TS`, `Cs_in[*]`) | mg/L | mg/L | Same. | + +> [!NOTE] +> Pending full node review (2026-05). Settler does **not** declare a `requireUnitForTypes` policy via MeasurementContainer; verify against the general unit policy before relying on internal canonicalisation. + +--- + +## Child registration + +Source: `src/specificClass.js` `configure()` (the `this.router.onRegister(...)` chain) and the three `_connect*` methods. + +| Software type | Filter | Wired to | Side-effect | +|:---|:---|:---|:---| +| `measurement` | any (no asset-type / position filter at register time) | `_connectMeasurement(child)` | Subscribes to `.measured.` on the child's `measurements.emitter`. Re-emits on settler's own MeasurementContainer (lets settler's parent see the value). `quantity (tss)` updates `C_TS`; anything else logs an `error` from `_updateMeasurement` but the re-emit still happened. | +| `reactor` | `positionVsParent === 'upstream'` (warns otherwise but still registers) | `_connectReactor(child)` | Stored as `this.upstreamReactor`. Listener attached **manually** to `reactor.emitter` (NOT `measurements.emitter`) for `'stateChange'`; on fire, settler pulls `reactor.getEffluent` and copies `F_in` + `Cs_in`. Handles both array and single-envelope `getEffluent` shapes. | +| `machine` | `positionVsParent === 'downstream'` (warns + skips otherwise) | `_connectMachine(child)` | Stored as `this.returnPump`. Settler reads `returnPump.measurements.type('flow').variant('measured').position('atEquipment').getCurrentValue()` to determine `F_sr`. Sets `machineChild.upstreamSource = this` so the pump can use settler's `inlet=2` Fluent as its suction-side context. | + +> [!NOTE] +> Pending full node review (2026-05). The `measurement` filter accepts any asset-type at register time; only the runtime `_updateMeasurement` switch acts on `quantity (tss)`. Other measurement types are silently re-emitted but not consumed. TODO: decide whether this is desired (pass-through telemetry) or a contract gap. + +### No virtual children + +Unlike `rotatingMachine`, settler does **not** auto-register any virtual measurement children. Every measurement must come from an explicitly wired child node. + +--- + +## Parent relationship + +Settler typically registers as `softwareType: 'settler'` with `positionVsParent: 'downstream'` against a reactor (the reactor's downstream stage). The downstream reactor consumes the three Fluent streams via `payload.inlet`: + +| `inlet` | Consumer expectation | +|:---|:---| +| `0` (clarified effluent) | Routed onward to the next process unit. | +| `1` (surplus sludge) | Typically routed to a sludge-handling process (digestion, thickening, dewatering). | +| `2` (return sludge) | Drawn back to a reactor inlet or the head of the biological train. | + +Multi-reactor settlers are **not supported** — `this.upstreamReactor` is a single slot; the last `child.register` call wins. + +--- + +## Related pages + +| Page | Why | +|:---|:---| +| [Home](Home) | Intuitive overview | +| [Reference — Architecture](Reference-Architecture) | Code map, reactor ↔ settler wiring, mass-balance math | +| [Reference — Examples](Reference-Examples) | Shipped flows + debug recipes | +| [Reference — Limitations](Reference-Limitations) | Known issues and open questions | +| [EVOLV — Topic Conventions](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions) | Platform-wide topic rules | +| [EVOLV — Telemetry](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Telemetry) | Port 0 / 1 / 2 InfluxDB layout | diff --git a/wiki/Reference-Examples.md b/wiki/Reference-Examples.md new file mode 100644 index 0000000..d97acfa --- /dev/null +++ b/wiki/Reference-Examples.md @@ -0,0 +1,139 @@ +# Reference — Examples + +![code-ref](https://img.shields.io/badge/code--ref-a3583a3-blue) + +> [!NOTE] +> Pending full node review (2026-05). The shipped example flows are **stub level**: each is a 4-node skeleton (tab + node + inject + debug) that proves the node loads in Node-RED but does **not** exercise the reactor → settler → return-pump chain or the TSS mass-balance math. Production-grade examples are TODO. See [Reference — Limitations — Example flows](Reference-Limitations#example-flows-are-stub-level). + +--- + +## Shipped examples + +| File | Tier | Dependencies | What it shows | +|:---|:---:|:---|:---| +| `basic.flow.json` | 1 (stub) | EVOLV only | Loads a single settler node, wires a `ping` inject to its input, taps Port 0 (only) to a debug node. Inject payload is the string `"1"` — will be rejected by `data.influent`'s payload validator (warn logged). Useful only to verify the node type registers. | +| `integration.flow.json` | 2 (stub) | EVOLV only | Same shape; inject sends `topic = registerChild`, payload `example-child-id`. The lookup will fail (no such node) and log a warn. Does **not** exercise reactor or pump wiring. | +| `edge.flow.json` | 3 (stub) | EVOLV only | Same shape; inject sends `topic = doesNotExist`. Verifies the registry rejects unknown topics. | + +### Status + +| Aspect | State | +|:---|:---| +| Loads in Node-RED on deploy | yes | +| Drives `data.influent` with a valid payload | no | +| Drives the reactor → settler → return-pump chain | no | +| Shows the 3-stream Fluent split on Port 0 | no | +| Has a dashboard tier | no | +| Validated against a live Node-RED instance | no | + +--- + +## Loading a flow + +### Via the editor + +1. Open the Node-RED editor at `http://localhost:1880`. +2. Menu → Import → drag the JSON file. +3. Click Deploy. + +### Via the Admin API + +```bash +curl -X POST -H 'Content-Type: application/json' \ + --data @nodes/settler/examples/basic.flow.json \ + http://localhost:1880/flows +``` + +--- + +## TODO — production-grade example set + +The matching `rotatingMachine` repo ships three tiers; settler needs the same. Tracking placeholder: + +| Tier | Proposed filename | What it should show | +|:---|:---|:---| +| 1 | `01 - Basic Manual Influent.json` | Single settler + inject sending `data.influent` with a realistic `{F, C}` payload. Three debug taps (one per Port 0 stream by `payload.inlet`). Operator can vary F and watch the split rebalance. | +| 2 | `02 - Reactor and Return Pump.json` | One `reactor` (upstream) + one `settler` + one `rotatingMachine` (return pump, downstream). Auto-registration via Port 2. Drive the reactor with an inject; settler should re-split on every reactor `stateChange`. | +| 3 | `03 - Dashboard Visualization.json` | FlowFuse Dashboard 2.0 page: F_in / F_eff / F_surplus / F_return trend chart, C_TS gauge, status badges per stream. Required: `@flowfuse/node-red-dashboard` installed. | + +> [!IMPORTANT] +> **Screenshots needed** once the production-grade examples land. Save as `wiki/_partial-screenshots/settler/01-basic-editor.png`, `02-reactor-pump-editor.png`, `03-dashboard-rendered.png`, ≤ 200 KB each. + +--- + +## What the basic example would do (sketch — not yet shipped) + +Operator workflow once a real Tier-1 ships: + +1. Deploy the flow. +2. Send `data.influent` with payload: + + ```json + { "F": 1000, "C": [0,0,0,0,0,0,0,30,80,400,200,80,3000] } + ``` + + Twelve soluble species at low concentrations + index 12 `X_TS = 3000` mg/L. +3. Observe three Port-0 messages arrive simultaneously, each with `topic = "Fluent"` and `payload.inlet` ∈ {0, 1, 2}. +4. With default `C_TS = 2500` mg/L: + - `F_s = 1000 * 3000 / 2500 = 1200` — but clamped to `F_in = 1000`. **The clamp fires** — this is the input edge case [Reference — Limitations — no-flow-balance warning](Reference-Limitations#no-flow-balance-warning) refers to. + - `F_eff = 0`. The clarified-effluent envelope carries zero flow but still has its `C` vector (with species 7–12 zeroed). + - `F_sr` is 0 (no pump wired) → `F_surplus = 1000`, `F_return = 0`. +5. Send `data.influent` with `X_TS = 1500` mg/L instead. Now `F_s = 600`, `F_eff = 400`, `F_surplus = 600`, `F_return = 0`. Realistic split. + +> [!NOTE] +> Pending full node review (2026-05). The clamp-fires behaviour above is intended (per code comment in `getEffluent`) but produces no operator-visible warning. Tracked. + +--- + +## Docker compose snippet + +To bring up Node-RED + InfluxDB with EVOLV nodes pre-loaded: + +```yaml +# docker-compose.yml (extract) +services: + nodered: + build: ./docker/nodered + ports: ['1880:1880'] + volumes: + - ./docker/nodered/data:/data/evolv + influxdb: + image: influxdb:2.7 + ports: ['8086:8086'] +``` + +Full file: [EVOLV/docker-compose.yml](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/docker-compose.yml). + +--- + +## Debug recipes + +> [!NOTE] +> Pending full node review (2026-05). Recipes below are grounded in the source; the symptom-side has not been confirmed against a live deployment. + +| Symptom | First thing to check | Where to look | +|:---|:---|:---| +| `F_eff` negative or `NaN` | `C_TS` is zero or `Cs_in[12]` is huge. The `F_s` clamp should prevent negatives — confirm the clamp `min(..., F_in)` is present. Likely the clamp fires but masks a deeper input problem. | `src/specificClass.js#getEffluent` line `const F_s = Math.min(...)`. | +| Settler never updates after reactor changes | Reactor child is not on `'upstream'` position (warn logged but registration proceeds), or the listener is attached to the wrong emitter. | `_connectReactor` — listens on `reactor.emitter`, **NOT** `reactor.measurements.emitter`. | +| Return-sludge flow stays at 0 | `returnPump.measurements.type('flow').variant('measured').position('atEquipment')` has no current value. Wire a flow measurement child on the pump (with `asset.type='flow'`, `positionVsParent='atEquipment'`). | `_connectMachine`, pump's MeasurementContainer chain. | +| Three Fluent envelopes do not arrive at the downstream consumer | `payload.inlet` selector on the downstream reactor / pump mismatches (0 = effluent, 1 = surplus, 2 = return). | The downstream consumer's inlet routing. | +| `quantity (tss)` updates don't change `C_TS` | Measurement child's `asset.type` must be the literal string `"quantity (tss)"` (with the space + parenthesised "tss"). | `src/specificClass.js#_updateMeasurement` switch case. | +| `data.influent` inject is silently dropped | Payload must be an object `{F, C}`. A string, number, or array logs a warn (`data.influent expects an object {F, C}; got `) and short-circuits. | `src/commands/handlers.js#dataInfluent`. | +| Settler logs an `error` `Type '' not recognized for measured update.` | A measurement child has registered with an `asset.type` settler doesn't recognise. The re-emit still happened — the error is about the absence of a `_updateMeasurement` switch case. Currently only `quantity (tss)` mutates state. | `_updateMeasurement`. | +| `child.register` topic logs `child.register skipped: missing child/source for id=` | The given node id doesn't resolve to a Node-RED node with a `.source` (i.e. an EVOLV domain). Verify the id is correct and the target node has already been deployed. | `src/commands/handlers.js#childRegister`. | +| Status badge stuck on `idle: no influent` | `F_in <= 0`. Either no reactor has fired `stateChange` yet, or `data.influent` has not been sent with a positive `F`. | `src/specificClass.js#getStatusBadge`. | + +> Never ship `enableLog: 'debug'` in a demo — fills the container log within seconds and obscures real errors. + +--- + +## Related pages + +| Page | Why | +|:---|:---| +| [Home](Home) | Intuitive overview | +| [Reference — Contracts](Reference-Contracts) | Topic + config + child filters | +| [Reference — Architecture](Reference-Architecture) | Code map, reactor ↔ settler wiring, mass-balance math | +| [Reference — Limitations](Reference-Limitations) | Known issues and open questions | +| [reactor — Examples](https://gitea.wbd-rd.nl/RnD/reactor/wiki/Reference-Examples) | The upstream parent — how to drive `stateChange` | +| [EVOLV — Topology Patterns](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topology-Patterns) | Where settler fits in a larger plant | diff --git a/wiki/Reference-Limitations.md b/wiki/Reference-Limitations.md new file mode 100644 index 0000000..30aae8b --- /dev/null +++ b/wiki/Reference-Limitations.md @@ -0,0 +1,116 @@ +# Reference — Limitations + +![code-ref](https://img.shields.io/badge/code--ref-a3583a3-blue) + +> [!NOTE] +> Pending full node review (2026-05). What `settler` does not do, current rough edges, and open questions. Open items live in `.agents/improvements/IMPROVEMENTS_BACKLOG.md` in the superproject. + +--- + +## When you would not use this node + +| Scenario | Use instead | +|:---|:---| +| Primary sedimentation upstream of biological treatment | The species 7–12 zeroing in the effluent stream is wrong for primary sludge (the soluble / particulate split is different). Model as a separate node. | +| Generic mass-balance transform | The 13-species ASM3 concentration vector is hard-coded; `Cs[12]` (`X_TS`) is the only species the split is keyed off. Not a fit for arbitrary stream-splitting. | +| Single-tank SBR with no separation stage | The 3-stream output expects a downstream consumer that routes by `payload.inlet`. A direct reactor → reactor wire is lighter. | +| A reactor — you want to model biological transformation, not separation | `reactor` (settler is a passive separator, not a biological process). | +| A return-pump itself — you want to model the pump's behaviour | `rotatingMachine` (settler reads the pump's measured flow but does not control it). | + +--- + +## Known limitations + +### Example flows are stub level + +The three shipped flows (`basic.flow.json`, `integration.flow.json`, `edge.flow.json`) are 4-node skeletons (tab + node + inject + debug). They prove the node loads in Node-RED but do not exercise the reactor → settler → pump chain, do not drive `data.influent` with a valid payload, and do not have a dashboard tier. Production-grade examples are TODO — see [Reference — Examples — TODO](Reference-Examples#todo--production-grade-example-set). + +### Editor colour drift + +`settler.html` declares `color: '#e4a363'` (orange). The S88 Unit level requires `#50a8d9` (blue). The placement-rule registry (`.claude/rules/node-red-flow-layout.md` §14) already maps `settler` to the `UN` lane regardless of editor colour, so demos lay out correctly; the cosmetic mismatch is tracked in §16 of the same rule and is on the colour-cleanup list. The wiki diagrams use the correct blue. + +### Single-reactor upstream slot + +`this.upstreamReactor` is a single slot. Registering a second `reactor` child with `positionVsParent='upstream'` silently overwrites the first — the listener on the previous reactor's `emitter` is not detached, so it keeps firing into a settler that will pull effluent from the new reactor instead. Tracked. + +### `X_TS` index is hard-coded + +The mass balance uses `Cs_in[12]` (the ASM3 `X_TS` lumped solids species) as the surrogate for total suspended solids. Any change to the species ordering in the upstream reactor breaks settler. The coupling is not documented in the schema — it lives only in the `getEffluent` math and the `_updateMeasurement` switch case for `quantity (tss)`. Tracked. + +### No flow-balance warning + +When influent solids exceed the target return concentration (`Cs_in[12] > C_TS`), `F_s` is clamped to `F_in` and clarified effluent drops to zero. This is mathematically correct but masks an upstream problem (overloaded reactor, miscalibrated `C_TS` setpoint). The clamp fires silently — no warn, no badge change beyond the eventual `F_in <= 0` idle state. Operator must monitor `F_eff` directly. Tracked. + +### `quantity (tss)` measurement passes through but doesn't validate + +`_connectMeasurement` re-emits every measurement type, but `_updateMeasurement` only acts on `quantity (tss)`. Other types log an `error` (`Type '' not recognized for measured update.`) but the re-emit already happened — the parent of settler still sees the value. Whether this is desired (settler acts as telemetry pass-through) or a contract gap is unresolved. + +> [!NOTE] +> Pending full node review (2026-05). Open question: should `_connectMeasurement` filter by asset-type at register time and reject non-`quantity (tss)` children, or continue accepting everything as pass-through telemetry? + +### No output manifest / no degraded-state coverage + +Per the platform output-coverage rule (`.claude/rules/output-coverage.md`), every node needs a `test/_output-manifest.md` enumerating every Port 0 / 1 / 2 key and a `test/basic/output-*.test.js` exercising each one in both populated **and** degraded states. Settler has neither. The most likely degraded-state crash points: + +- Port 0 emitted before any reactor `stateChange` — `F_in = 0`, `Cs_in = [0...]`, the three envelopes carry zero flow but valid (zero) `C` arrays. Should not crash a downstream consumer, but un-tested. +- Port 0 with `returnPump` registered but no flow measurement landed yet — `returnPump.measurements.type('flow').variant('measured').position('atEquipment').getCurrentValue()` returns `undefined` → `Math.min(undefined, F_s) = NaN`. `F_sr` becomes `NaN` → both `inlet=1` and `inlet=2` carry `NaN` flow. + +Tracked. TODO before trial-readiness: add the manifest, tests, and `null`-flow-measurement handling. + +### `reactor.getEffluent` shape coupling + +`_connectReactor` does `Array.isArray(raw) ? raw[0] : raw` to absorb both the older single-envelope shape and the newer 3-stream array shape of `reactor.getEffluent`. The 2026-03-02 fix is the only thing keeping the older shape alive in production. If `reactor.getEffluent` ever returns a 3-stream array and settler should consume `inlet=0` specifically, the current `raw[0]` selector works by accident — it picks the first envelope regardless of inlet number. Open question whether to make the selection inlet-aware. + +### Stateful telemetry — no decay / no TTL + +The MeasurementContainer holds the last-known value of every re-emitted measurement forever. If a child stops publishing, the stale value persists on Port 1 indefinitely. There is no TTL, no `data.clear-measurement` topic, and no health flag like `rotatingMachine`'s `predictionQuality`. Open question. + +--- + +## Open questions (tracked) + +| Question | Where it lives | +|:---|:---| +| Filter `_connectMeasurement` by asset-type at register time? | Internal — not yet ticketed | +| Multi-reactor upstream support — teardown ordering, listener detach | Internal | +| Flow-balance warning when `F_s` clamp fires | Internal | +| Inlet-aware selection in `_connectReactor` shape handling | Internal | +| Measurement TTL / staleness flag | Internal | +| Production-grade example flows (Tier 1 / 2 / 3) | `.agents/improvements/IMPROVEMENTS_BACKLOG.md` | +| Output manifest + degraded-state tests | `.claude/rules/output-coverage.md` (platform-wide rule) | +| Editor colour cleanup (`#e4a363` → `#50a8d9`) | `.claude/rules/node-red-flow-layout.md` §16 | + +--- + +## Migration notes + +> [!NOTE] +> Pending full node review (2026-05). No structural migrations have been performed on settler since the AssetResolver refactor of rotatingMachine; the notes below document the one historical fix on record. + +### From pre-2026-03-02 `_connectReactor` + +Before 2026-03-02 `_connectReactor` assumed `reactor.getEffluent` always returned an array, and indexed `[0]` unconditionally. After the reactor refactor, `getEffluent` returns a single envelope — pre-fix settler would crash with `Cannot read properties of undefined (reading 'payload')`. The fix: + +```js +const raw = this.upstreamReactor.getEffluent; +const effluent = Array.isArray(raw) ? raw[0] : raw; +``` + +If you maintain a fork of settler from before that date, port this guard. + +### From topic-aliased payloads + +Both `influent` and `setInfluent` are accepted as aliases for `data.influent`. A one-time deprecation warning fires the first time each alias is seen. Tracked for removal; use the canonical `data.influent` in new flows. + +--- + +## Related pages + +| Page | Why | +|:---|:---| +| [Home](Home) | Intuitive overview | +| [Reference — Contracts](Reference-Contracts) | Topic + config + child filters | +| [Reference — Architecture](Reference-Architecture) | Code map, reactor ↔ settler wiring, mass-balance math | +| [Reference — Examples](Reference-Examples) | Shipped flows + the TODO list for production-grade demos | +| [reactor — Limitations](https://gitea.wbd-rd.nl/RnD/reactor/wiki/Reference-Limitations) | The upstream parent — effluent shape contract | +| [EVOLV — output-coverage rule](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/.claude/rules/output-coverage.md) | Platform-wide output-coverage requirement | diff --git a/wiki/_Sidebar.md b/wiki/_Sidebar.md new file mode 100644 index 0000000..1865556 --- /dev/null +++ b/wiki/_Sidebar.md @@ -0,0 +1,19 @@ +### settler + +- [Home](Home) + +**Reference** + +- [Contracts](Reference-Contracts) +- [Architecture](Reference-Architecture) +- [Examples](Reference-Examples) +- [Limitations](Reference-Limitations) + +**Related** + +- [EVOLV master wiki](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Home) +- [reactor wiki](https://gitea.wbd-rd.nl/RnD/reactor/wiki/Home) +- [rotatingMachine wiki](https://gitea.wbd-rd.nl/RnD/rotatingMachine/wiki/Home) +- [Topology Patterns](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topology-Patterns) +- [Topic Conventions](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions) +- [Telemetry](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Telemetry)