From 76951f104da16e1af5678e208852e3211bf5e94a Mon Sep 17 00:00:00 2001 From: znetsixe Date: Tue, 19 May 2026 09:42:13 +0200 Subject: [PATCH] docs(wiki): full 5-page wiki matching the rotatingMachine reference format Replaces the prior stub/partial wiki with a Home + Reference-{Architecture, Contracts,Examples,Limitations} + _Sidebar structure. Topic-contract and data-model sections wrapped in AUTOGEN markers for the future wiki-gen tool. Source-vs-spec contradictions surfaced and flagged inline (not silently fixed). Pending-review notes mark sections that need a full node review. Co-Authored-By: Claude Opus 4.7 (1M context) --- wiki/Home.md | 380 ++++++++++----------------------- wiki/Reference-Architecture.md | 245 +++++++++++++++++++++ wiki/Reference-Contracts.md | 204 ++++++++++++++++++ wiki/Reference-Examples.md | 156 ++++++++++++++ wiki/Reference-Limitations.md | 127 +++++++++++ wiki/_Sidebar.md | 21 ++ 6 files changed, 870 insertions(+), 263 deletions(-) create mode 100644 wiki/Reference-Architecture.md create mode 100644 wiki/Reference-Contracts.md create mode 100644 wiki/Reference-Examples.md create mode 100644 wiki/Reference-Limitations.md create mode 100644 wiki/_Sidebar.md diff --git a/wiki/Home.md b/wiki/Home.md index cb83f8e..6f48eb8 100644 --- a/wiki/Home.md +++ b/wiki/Home.md @@ -1,13 +1,28 @@ # monster -> **Reflects code as of `8540328` · 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-cd185dc-blue) ![s88](https://img.shields.io/badge/S88-Unit-50a8d9) ![status](https://img.shields.io/badge/status-stub--tier-orange) -## 1. What this node is +A `monster` models the physical "monsternamekast" (composite-sampling cabinet) on a wastewater treatment plant. It runs an AQUON-scheduled, flow-proportional sampling program: aggregates measured + manual flow, blends in a rain-scaled prediction, integrates volume, and emits a `pulse` event whenever the integrated volume crosses `m³ per pulse`. Used downstream of `reactor` / `settler` for compliance + process diagnostics — but note that the node only **integrates flow** in the current implementation; multi-constituent reporting (NH4, NO3, COD, TSS, …) is a downstream consumer concern, not produced by this node. -**monster** is an S88 Unit that runs an AQUON-scheduled flow-proportional sampling program. It aggregates measured + manual flow, blends a rain-scaled prediction, and emits a `pulse` whenever the integrated volume crosses `m³ per pulse`. Drives the physical "monsternamekast" (composite sampling cabinet) on a wastewater treatment plant. +> [!NOTE] +> Pending full node review (2026-05). Content reflects `CONTRACT.md` and current source only. -## 2. Position in the platform +--- + +## At a glance + +| Thing | Value | +|:---|:---| +| What it represents | One composite-sampling cabinet running AQUON-scheduled, flow-proportional sampling | +| S88 level | Unit | +| Use it when | You need scheduled composite samples with flow-proportional pulses for a benchtop analyser / lab pickup | +| Don't use it for | Generic flow totalising (use `measurement`), ad-hoc grab samples (fire `cmd.start` once instead), or analyser results — monster emits pulses, not analyte values | +| Children it accepts | `measurement` with `asset.type='flow'` (or unset) | +| Parents it talks to | Any node that issues `cmd.start` / `set.schedule` / `set.rain` / `data.flow` (typically a Process Cell coordinator) | + +--- + +## How it fits ```mermaid flowchart LR @@ -15,304 +30,143 @@ flowchart LR monster[monster
Unit]:::unit flow_up[measurement
type=flow
position=upstream]:::ctrl flow_at[measurement
type=flow
position=atequipment]:::ctrl + flow_dn[measurement
type=flow
position=downstream]:::ctrl op[(operator / AQUON)] weather[(Open-Meteo)] flow_up -->|flow.measured.upstream| monster flow_at -->|flow.measured.atequipment| monster + flow_dn -->|flow.measured.downstream| monster op -->|set.schedule / data.flow / cmd.start| monster weather -->|set.rain| monster monster -->|child.register| parent - monster -.evt.output.-> parent + monster -.evt output (pulse / bucketVol).-> parent classDef pc fill:#0c99d9,color:#fff classDef unit fill:#50a8d9,color:#000 classDef ctrl fill:#a9daee,color:#000 ``` -S88 colours: Process Cell `#0c99d9`, Unit `#50a8d9`, 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 editor node tile in `monster.html` is currently `#4f8582` (teal) and pending cleanup — Section 16 of the layout rules tracks it. -## 3. Capability matrix +--- -| Capability | Status | Notes | -|---|---|---| -| Flow-proportional sampling pulse | ✅ | Triggered when integrated `temp_pulse` ≥ 1. | -| Time-bounded sampling run | ✅ | Run length governed by `constraints.samplingtime` (hours). | -| AQUON schedule auto-start | ✅ | `set.schedule` parses AQUON rows; `nextDate` arms the next run. | -| Rain-scaled flow prediction | ✅ | `set.rain` aggregates Open-Meteo hourly precipitation into `sumRain` / `avgRain`. | -| Measured + manual flow blend | ✅ | `flowTracker.getEffectiveFlow()` picks measured if recent, else manual. | -| Minimum sample-interval cooldown | ✅ | Skips pulses inside `minSampleIntervalSec` window. | -| Bucket / weight tracking | ✅ | `bucketVol`, `bucketWeight` track the composite container. | -| Sub-sample volume override | ❌ | `subSampleVolume` fixed at 50 mL per schema. | -| Stateful FSM | ❌ | Monster is run/idle only — no formal state machine. | +## Try it — 3-minute demo -## 4. Code map +Import the basic example flow, deploy, and drive the sampler through a single run. -```mermaid -flowchart TB - subgraph nodeRED["nodeClass.js — adapter (BaseNodeAdapter)"] - nc["buildDomainConfig()
static DomainClass = Monster
static commands"] - end - subgraph domain["specificClass.js — orchestrator (BaseDomain)"] - sc["Monster.configure()
wires concerns, ChildRouter rules
tick() → flowCalc → samplingProgram"] - end - subgraph concerns["src/ concern modules"] - parameters["parameters/
bounds + targets + rain index"] - flow["flow/
FlowTracker (measured + manual)"] - rain["rain/
RainAggregator (sum + avg)"] - schedule["schedule/
AQUON next-date parser"] - sampling["sampling/
samplingProgram + flowCalc"] - io["io/
output + statusBadge"] - commands["commands/
topic registry + handlers"] - end - nc --> sc - sc --> parameters - sc --> flow - sc --> rain - sc --> schedule - sc --> sampling - sc --> io - nc --> commands +```bash +curl -X POST -H 'Content-Type: application/json' \ + --data @nodes/monster/examples/basic.flow.json \ + http://localhost:1880/flow ``` -| Module | Owns | Read first if you're changing… | -|---|---|---| -| `parameters/` | Bounds (`minPuls`, `absMaxPuls`, `targetPuls`), rain-index lookup, predicted-flow rate. | Sampling bounds, rain scaling math. | -| `flow/` | `FlowTracker` — measured-child latch + manual flow + effective-flow pick. | Flow source priority, dead-band logic. | -| `rain/` | `RainAggregator` — fold hourly Open-Meteo rows into `sumRain` / `avgRain`. | Rain inputs, forecast horizon. | -| `schedule/` | AQUON schedule parsing, `nextDate` + `daysPerYear` derivation. | Schedule arming, sample-name filtering. | -| `sampling/` | `_beginRun` / `_endRun` / `_maybeEmitPulse` — pulse integrator + cooldown guard. | Pulse timing, cooldown behaviour. | -| `io/` | `buildOutput`, `buildStatusBadge`. | Port-0 payload shape, status badge text. | -| `commands/` | Topic registry + payload validation + unit conversion. | New input topics, alias deprecation. | +What to click after deploy (the inject buttons map to the topics in [Reference — Contracts](Reference-Contracts#topic-contract)): -## 5. Topic contract +1. `set.rain` — push an Open-Meteo precipitation snapshot so `sumRain` / `avgRain` populate (otherwise the rain-scaled prediction stays at `nominalFlowMin`). +2. `set.schedule` — push an AQUON row array so `nextDate` arms. +3. `data.flow = {value: 240, unit: 'm3/h'}` — supply a manual flow value (or wire a `measurement` child for the measured path). +4. `cmd.start = true` — releases the start gate. On the next 1000 ms tick the sampling program checks `validateFlowBounds`; if `nominalFlowMin < flowMax`, `_beginRun` fires, `running` flips to `true`, and the bucket integrator starts. +5. Watch Port 0: `pulse` blips to `true` for one tick whenever `temp_pulse` crosses 1; `bucketVol` rises in 50 mL steps (the hard-coded `subSampleVolume`); `sumPuls` increments. +6. After `constraints.samplingtime` hours `_endRun` fires and `running` flips to `false`. -> **Auto-generated** from `src/commands/index.js`. Do NOT hand-edit between the markers. Re-run `npm run wiki:contract`. +> [!IMPORTANT] +> **GIF needed.** Demo recording of steps 1–6 with the live status panel. Save as `wiki/_partial-gifs/monster/basic-demo.gif`, target ≤ 1 MB after `gifsicle -O3 --lossy=80`. - +--- -| Canonical topic | Aliases | Payload | Unit | Effect | -|---|---|---|---|---| -| `cmd.start` | `i_start` | `any` | — | Trigger / release the sampler start gate. | -| `set.schedule` | `monsternametijden` | `any` | — | Replace the sampling-times schedule. | -| `set.rain` | `rain_data` | `any` | — | Push current rain-event data into the sampler logic. | -| `data.flow` | `input_q` | `object` | — | Push the upstream flow measurement (payload: {value, unit}). | -| `set.mode` | `setMode` | `any` | — | Switch the monster between auto / manual modes. | -| `set.model-prediction` | `model_prediction` | `any` | — | Push the upstream rain-prediction snapshot used by the sampler. | -| `child.register` | `registerChild` | `string` | — | Register a child node (typically a measurement) with this monster. | +## The seven things you'll send - +| Topic | Aliases | Payload | What it does | +|:---|:---|:---|:---| +| `cmd.start` | `i_start` | truthy / falsy | Sets `source.i_start`. On the next tick a sampling run begins if `validateFlowBounds` passes. | +| `set.schedule` | `monsternametijden` | array of AQUON rows (`SAMPLE_NAME`, `DESCRIPTION`, `SAMPLED_DATE`, `START_DATE`, `END_DATE`) | Stores the schedule and recomputes `nextDate` + `daysPerYear` for the configured `aquonSampleName`. | +| `set.rain` | `rain_data` | per-location rain forecast (Open-Meteo shape) | Aggregates hourly precipitation into `sumRain` / `avgRain`; feeds the rain-scaled flow prediction. Ignored while `running=true`. | +| `data.flow` | `input_q` | `{ value: number, unit: string }` | Converts to m³/h and pushes into `flow.manual.atequipment`. Blends with measured-child flow in `getEffectiveFlow()`. | +| `set.mode` | `setMode` | string | Reserved — handler delegates to `source.setMode()` which is currently undefined. No-op today. | +| `set.model-prediction` | `model_prediction` | numeric | Reserved — handler delegates to `source.setModelPrediction()` which is currently undefined. No-op today. | +| `child.register` | `registerChild` | string (child node id) | Register a `measurement` child. Port 2 wiring does this automatically. | -## 6. Child registration +There is no `query.*` topic on monster (unlike `rotatingMachine`'s `query.curves` / `query.cog`). All state is on Port 0. -Mirrors the `ChildRouter` declaration in `specificClass.js → configure()`. Monster only accepts `measurement` children whose `asset.type` is `flow` (or unset). +--- -```mermaid -flowchart LR - subgraph kids["accepted children (softwareType)"] - m_up["measurement
asset.type=flow
position=upstream"]:::ctrl - m_at["measurement
asset.type=flow
position=atequipment"]:::ctrl - m_dn["measurement
asset.type=flow
position=downstream"]:::ctrl - end - m_up -->|flow.measured.upstream| ft[FlowTracker.handleMeasuredFlow] - m_at -->|flow.measured.atequipment| ft - m_dn -->|flow.measured.downstream| ft - ft --> eff[getEffectiveFlow] - classDef ctrl fill:#a9daee,color:#000 -``` +## What you'll see come out -| softwareType | filter | wired to | side-effect | -|---|---|---|---| -| `measurement` | `asset.type=flow` (or missing) | `flowTracker.handleMeasuredFlow` | Latches latest measured flow for `getEffectiveFlow`. | -| `measurement` | `asset.type` anything else | _ignored_ | Logged once at register. | - -## 7. Lifecycle — the sampling-program loop - -```mermaid -sequenceDiagram - participant child as measurement child - participant ops as operator / AQUON - participant monster as monster - participant ft as flowTracker - participant sp as samplingProgram - participant out as Port-0 output - - child->>monster: flow.measured. - ops->>monster: set.schedule / cmd.start / data.flow - Note over monster: every 1000 ms tick - monster->>ft: getEffectiveFlow() - monster->>monster: flowCalc → m3PerTick - monster->>sp: samplingProgram - alt cmd.start OR Date.now() ≥ nextDate - sp->>sp: validateFlowBounds → _beginRun - end - alt running AND stop_time > now - sp->>sp: integrate temp_pulse += m3PerTick / m3PerPuls - sp->>sp: _maybeEmitPulse (cooldown-guarded) - else stop_time elapsed - sp->>sp: _endRun - end - monster->>out: msg{topic, payload (delta-compressed)} -``` - -One pulse per integrated `m³ per pulse`. The cooldown guard suppresses pulses inside `minSampleIntervalSec` and increments `missedSamples`. - -## 8. Data model — `getOutput()` - -What lands on Port 0. Built in `buildOutput()` from `m.measurements.getFlattenedOutput()` plus the sampling-run snapshot. - - - -| Key | Type | Unit | Sample | -|---|---|---|---| -| `avgRain` | number | — | `0` | -| `bucketVol` | number | — | `0` | -| `bucketWeight` | number | — | `0` | -| `daysPerYear` | number | — | `0` | -| `flowMax` | number | — | `0` | -| `flowToNextPulseM3` | number | — | `0` | -| `invalidFlowBounds` | boolean | — | `false` | -| `m3PerPuls` | number | — | `0` | -| `m3PerPulse` | number | — | `0` | -| `m3Total` | number | — | `0` | -| `maxVolume` | number | — | `20` | -| `minSampleIntervalSec` | number | — | `60` | -| `minVolume` | number | — | `5` | -| `missedSamples` | number | — | `0` | -| `nextDate` | undefined | — | `null` | -| `nominalFlowMin` | number | — | `0` | -| `predFlow` | number | — | `0` | -| `predM3PerSec` | number | — | `0` | -| `predictedRateM3h` | number | — | `0` | -| `pulse` | boolean | — | `false` | -| `pulseFraction` | number | — | `0` | -| `pulsesRemaining` | number | — | `200` | -| `q` | number | — | `0` | -| `running` | boolean | — | `false` | -| `sampleCooldownMs` | number | — | `0` | -| `sumPuls` | number | — | `0` | -| `sumRain` | number | — | `0` | -| `targetDeltaL` | number | — | `-10` | -| `targetDeltaM3` | number | — | `-0.01` | -| `targetProgressPct` | number | — | `0` | -| `targetVolumeM3` | number | — | `0.01` | -| `timeLeft` | number | — | `0` | -| `timePassed` | number | — | `0` | -| `timeToNextPulseSec` | number | — | `0` | - - - -**Concrete sample** (mid-run snapshot): +Sample Port 0 message (mid-run snapshot, delta-compressed): ```json { - "pulse": false, - "running": true, - "bucketVol": 1.25, - "sumPuls": 25, - "predFlow": 240.0, - "m3PerPuls": 4, - "q": 215.4, - "timeLeft": 12340, - "targetVolumeM3": 0.005, - "targetProgressPct": 41.6, - "sumRain": 3.2, - "avgRain": 0.13, - "nextDate": 1746940800000 + "topic": "monster#cabinet_1", + "payload": { + "running": true, + "pulse": false, + "bucketVol": 1.25, + "bucketWeight": 4.25, + "sumPuls": 25, + "m3PerPuls": 4, + "m3Total": 100.0, + "q": 215.4, + "predFlow": 240.0, + "predM3PerSec": 0.067, + "timeLeft": 12340, + "timePassed": 600, + "targetVolumeM3": 0.005, + "targetProgressPct": 41.6, + "targetDeltaL": -2.93, + "predictedRateM3h": 240.0, + "sumRain": 3.2, + "avgRain": 0.13, + "nextDate": 1746940800000, + "daysPerYear": 12, + "missedSamples": 0, + "sampleCooldownMs": 0, + "invalidFlowBounds": false + } } ``` -Concrete samples must come from a known-good test run. Regenerate when concern modules change shape. +| Field | Meaning | +|:---|:---| +| `running` | Run/idle flag — monster has no formal FSM, just this boolean and the timer fields below. | +| `pulse` | `true` for the tick on which a pulse is emitted; falls back to `false` on the next tick. | +| `bucketVol` / `bucketWeight` | Accumulated composite volume (L) and total bucket mass (kg = vol + `emptyWeightBucket`). | +| `sumPuls` / `pulsesRemaining` | Pulses fired this run vs target ceiling. | +| `m3PerPuls` | Volume per pulse, set at `_beginRun` from `predFlow / targetPuls`. | +| `m3Total` | Total integrated flow this run (m³). | +| `q` | Effective flow (m³/h) — `flowTracker.getEffectiveFlow()` blends measured + manual. | +| `predFlow` / `predM3PerSec` | Predicted total volume over the run window (m³) and its average rate. | +| `timeLeft` / `timePassed` | Run-window seconds remaining / elapsed. | +| `targetVolumeM3` / `targetProgressPct` / `targetDeltaL` | Target composite volume (m³), % of target accumulated, signed L delta against target. | +| `predictedRateM3h` | Rain-scaled flow prediction (m³/h) — between `nominalFlowMin` and `flowMax`. | +| `sumRain` / `avgRain` | Probability-weighted hourly precipitation, summed / averaged across locations. | +| `nextDate` / `daysPerYear` | Next scheduled run epoch ms; count of remaining runs this calendar year. | +| `missedSamples` / `sampleCooldownMs` | Cooldown-blocked pulse count; ms remaining on the active cooldown. | +| `invalidFlowBounds` | True when `nominalFlowMin >= flowMax` — gates `_beginRun`. | -## 9. Configuration — editor form ↔ config keys +Full Port-0 key list: see [Reference — Contracts — Data model](Reference-Contracts#data-model--getoutput-shape). -```mermaid -flowchart TB - subgraph editor["Node-RED editor form"] - f1[Sampling time hr] - f2[Sampling period hr] - f3[Min volume L] - f4[Max weight kg] - f5[Empty bucket weight kg] - f6[Flowmeter on/off] - f7[Min sample interval s] - end - subgraph config["Domain config slice"] - c1[constraints.samplingtime] - c2[constraints.samplingperiod] - c3[constraints.minVolume] - c4[constraints.maxWeight] - c5[asset.emptyWeightBucket] - c6[constraints.flowmeter] - c7[constraints.minSampleIntervalSec] - end - f1 --> c1 - f2 --> c2 - f3 --> c3 - f4 --> c4 - f5 --> c5 - f6 --> c6 - f7 --> c7 -``` +Key shape is **flat scalars on the snapshot** plus a `MeasurementContainer.getFlattenedOutput()` flush of `flow..` entries. monster does **not** use the four-segment `...` shape that `rotatingMachine` emits — no `childId` is appended because monster fans incoming children into the same three positions and the latest value wins. -| Form field | Config key | Default | Range | Where used | -|---|---|---|---|---| -| Sampling time (hr) | `constraints.samplingtime` | `0` | ≥ 0 | run length, `predFlow` | -| Sampling period (hr) | `constraints.samplingperiod` | `24` | ≥ 1 | schedule arming | -| Min volume (L) | `constraints.minVolume` | `5` | ≥ 5 | bounds + invalid-flow guard | -| Max weight (kg) | `constraints.maxWeight` | `23` | ≤ 23 | bucket overload | -| Empty bucket weight (kg) | `asset.emptyWeightBucket` | `3` | ≥ 0 | `bucketWeight` | -| Sub-sample volume (mL) | `constraints.subSampleVolume` | `50` | fixed | per-pulse volume | -| Flowmeter present | `constraints.flowmeter` | `true` | bool | ⚠️ In schema but NOT wired in `buildDomainConfig` — effectively always `true` at runtime. | -| Min sample interval (s) | `constraints.minSampleIntervalSec` | `60` | ≥ 0 | cooldown guard | -| Nominal flow min (m³/h) | `constraints.nominalFlowMin` | `0` | ≥ 0 | lower bound for rain-driven flow prediction band | -| Flow max (m³/h) | `constraints.flowMax` | `0` | ≥ 0 | upper bound for rain-driven flow prediction band | -| Max rain reference | `constraints.maxRainRef` | `10` | > 0 | rain-index that maps to `flowMax` | -| Intake speed (m/s) | `constraints.intakeSpeed` | `0.3` | ≥ 0 | informational | -| Intake diameter (mm) | `constraints.intakeDiameter` | `12` | ≥ 0 | informational | +--- -## 10. State chart +## Status badge -Skipped — monster has no formal state machine. The `running` boolean toggles when `_beginRun` / `_endRun` fire. See section 7 for the sampling-program sequence, which is the closest analogue. +| State | Badge | Fill | +|:---|:---|:---| +| `invalidFlowBounds=true` | `Config error: nominalFlowMin (…) >= flowMax (…)` | red | +| `running=true` + cooldown active | `SAMPLING (Ns) · / L` | yellow ring | +| `running=true` + normal | `AI: RUNNING · / L` | green dot | +| idle | `AI: IDLE` | grey ring | -## 11. Examples +--- -| Tier | File | What it shows | Status | -|---|---|---|---| -| Basic | `examples/basic.flow.json` | Inject + manual flow + dashboard, no parent | ✅ in repo | -| Integration | `examples/integration.flow.json` | monster + measurement child + AQUON schedule | ✅ in repo | -| Dashboard | `examples/monster-dashboard.flow.json` | Live FlowFuse charts (pulse, bucket, predFlow) | ✅ in repo | -| Edge | `examples/edge.flow.json` | Cooldown guard + invalid-flow bounds | ✅ in repo | -| API | `examples/monster-api-dashboard.flow.json` | dashboardAPI consumer wired in | ✅ in repo | +## Need more? -One screenshot per tier where helpful. PNG ≤ 200 KB under `wiki/_partial-screenshots/monster/`. +| Page | What you'll find | +|:---|:---| +| [Reference — Contracts](Reference-Contracts) | Full topic contract, config schema, child registration filters | +| [Reference — Architecture](Reference-Architecture) | Code map, sampling-program loop, prediction + cooldown pipeline | +| [Reference — Examples](Reference-Examples) | Shipped example flows + debug recipes | +| [Reference — Limitations](Reference-Limitations) | When not to use, known limitations, open questions | -## 12. Debug recipes - -| Symptom | First thing to check | Where to look | -|---|---|---| -| `pulse` never fires | Is `running` true? Check `validateFlowBounds` log — invalid bounds short-circuits the run. | `parameters/parameters.js` | -| Pulses arrive too fast / skipped | Cooldown guard active. Inspect `missedSamples` + `minSampleIntervalSec`. | `sampling/samplingProgram.js → _maybeEmitPulse` | -| `q` always zero | Measured-flow child not registered, manual flow not pushed. Watch Port 2 + `data.flow` history. | `flow/flowTracker.js` | -| Measurement child ignored at register | Child's `config.asset.type` is set to something other than `"flow"` (e.g. `"flow-electromagnetic"`). Monster only accepts `asset.type = "flow"` or unset. Set `asset.type: "flow"` on the measurement node. | `specificClass.js → _wireMeasurementChild` | -| `nextDate` not arming | `set.schedule` payload didn't include matching `aquonSampleName` row. | `schedule/schedule.js → regNextDate` | -| `sumRain` zero with rain input | `rainAggregator.update` ran while `running=true` (skipped). | `specificClass.js → updateRainData` | -| Bucket overfilled before `stop_time` | `m3PerPuls` rounded down; check `predFlow` vs effective `q`. | `sampling/samplingProgram.js → _beginRun` | -| `flowmeter=false` has no effect | `constraints.flowmeter` is in the schema but not wired in `buildDomainConfig`. The sampling program always runs in proportional mode. | `src/nodeClass.js → buildDomainConfig` | - -> Never ship `enableLog: 'debug'` in a demo — fills the container log within seconds and obscures real errors. Use only for live debugging. - -## 13. When you would NOT use this node - -- Use monster for **AQUON-scheduled composite sampling** on a wastewater plant. For a single grab sample on demand, fire `cmd.start` once and tear it down — monster is overkill for ad-hoc work. -- Don't use monster as a generic flow totaliser. Use a `measurement` child with the right type/variant for raw flow integration. -- Skip monster if you don't need pulse-proportional dosing — the time-mode (`flowmeter=false`) is currently informational only. - -## 14. Known limitations / current issues - -| # | Issue | Tracked in | -|---|---|---| -| 1 | Edge test `sampling-guards.edge.test.js` cooldown-guard case is a pre-existing failure — the cooldown skip increments `missedSamples` but the assertion expects a different timing. | `test/edge/sampling-guards.edge.test.js` | -| 2 | `set.mode` and `set.model-prediction` are reserved — handlers delegate to optional methods that don't exist yet. | `commands/handlers.js` | -| 3 | Time-only mode (`flowmeter=false`) is not exercised — the sampling program assumes a flow source. `constraints.flowmeter` is also not forwarded in `buildDomainConfig`. | `sampling/samplingProgram.js`, `src/nodeClass.js` | -| 4 | Sub-sample volume hard-coded at 50 mL (schema enforces `min=max=50`). | `generalFunctions/src/configs/monster.json` | -| 5 | S88 colour cleanup pending: node editor colour is `#4f8582` (teal) in `monster.html`. Correct Unit-level colour is `#50a8d9`. Section 2 diagram already uses the correct colour. The editor node tile must be updated separately. | `monster.html` → `color:` field | +[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..ff35a5b --- /dev/null +++ b/wiki/Reference-Architecture.md @@ -0,0 +1,245 @@ +# Reference — Architecture + +![code-ref](https://img.shields.io/badge/code--ref-cd185dc-blue) + +> [!NOTE] +> Code structure for `monster`: the three-tier sandwich, the `src/` layout, the sampling-program loop, the rain-scaled flow prediction, the cooldown guard, the lifecycle, and the output-port pipeline. For an intuitive overview, return to [Home](Home). + +> [!NOTE] +> Pending full node review (2026-05). Content reflects `CONTRACT.md` and current source only. + +--- + +## Three-tier code layout + +``` +nodes/monster/ +| ++-- monster.js entry: RED.nodes.registerType('monster', NodeClass) +| ++-- src/ +| nodeClass.js extends BaseNodeAdapter (Node-RED bridge) +| specificClass.js extends BaseDomain (orchestration only) +| | +| +-- commands/ +| | index.js topic descriptors (cmd.start / set.schedule / …) +| | handlers.js pure handler functions +| | +| +-- parameters/ +| | parameters.js applyBoundsAndTargets / validateFlowBounds / +| | getRainIndex / getPredictedFlowRate / +| | getSampleCooldownMs +| | +| +-- flow/ +| | flowTracker.js measured-child latch + manual flow blend +| | +| +-- rain/ +| | rainAggregator.js Open-Meteo precipitation fold (sumRain / avgRain) +| | +| +-- schedule/ +| | schedule.js AQUON next-date parser + daysPerYear count +| | +| +-- sampling/ +| | samplingProgram.js _beginRun / _endRun / _maybeEmitPulse / +| | flowCalc / samplingProgram / getModelPrediction +| | +| +-- io/ +| output.js buildOutput() — Port 0 snapshot +| statusBadge.js buildStatusBadge() — editor badge composer +``` + +### Tier responsibilities + +| Tier | File | What it owns | Touches `RED.*` | +|:---|:---|:---|:---:| +| entry | `monster.js` | Type registration; `/monster/menu.js` + `/monster/configData.js` HTTP endpoints | Yes | +| nodeClass | `src/nodeClass.js` | `tickInterval = 1000` (sampling integrator needs wall-clock delta), `statusInterval = 1000`, `buildDomainConfig` slice, `extraSetup` propagates `aquonSampleName` | Yes | +| specificClass | `src/specificClass.js` | Wire concern modules in `configure()`; expose the public surface legacy tests call (`monster.bucketVol`, `monster.q`, `monster.sampling_program()`, …); delegate everything else | No | + +`specificClass` is orchestration. Real work lives in the concern modules: pure math in `parameters/`, schedule walking in `schedule/`, aggregation in `rain/`, the sampling integrator in `sampling/`. + +--- + +## Sampling program — the time-driven core + +monster has **no formal FSM**. The `running` boolean toggles when `_beginRun` / `_endRun` fire. The closest analogue to a state diagram is the per-tick decision tree: + +```mermaid +flowchart TB + tick[tick() — every 1000 ms] + tick --> q[q = flowTracker.getEffectiveFlow()] + q --> fc[flowCalc()
m3PerTick = q/3600 * dt] + fc --> sp[samplingProgram()] + sp --> trig{i_start OR
now ≥ nextDate?
AND NOT running} + trig -- yes --> vb{validateFlowBounds?} + vb -- no --> done[return — running stays false] + vb -- yes --> begin[_beginRun
m3PerPuls = predFlow / targetPuls
stop_time = now + samplingtime·h] + trig -- no --> active{stop_time > now?} + begin --> active + active -- yes --> integ[temp_pulse += m3PerTick / m3PerPuls
m3Total += m3PerTick] + integ --> pulse[_maybeEmitPulse] + pulse --> emit{temp_pulse ≥ 1
AND sumPuls < absMaxPuls?} + emit -- no --> notify[notifyOutputChanged] + emit -- yes --> cd{cooldown
blocked?} + cd -- yes --> miss[missedSamples++
warn one-shot] + miss --> notify + cd -- no --> fire[temp_pulse -= 1
pulse = true
sumPuls++
bucketVol += 50 mL] + fire --> notify + active -- no, running --> endR[_endRun — running = false] + endR --> notify +``` + +Key invariants: + +- One pulse per integrated `m³ per pulse`. The cooldown guard suppresses pulses inside `minSampleIntervalSec` and increments `missedSamples` (without rolling the integrator back — `temp_pulse` is clamped to `1` so subsequent ticks land on the same threshold once the cooldown clears). +- `_beginRun` rounds `m3PerPuls = round(predFlow / targetPuls)`. With low `predFlow` this can round to 0; the integrator then divides by zero and `temp_pulse` becomes `Infinity`. Tracked — see [Limitations](Reference-Limitations#mperpuls-can-round-to-zero). +- `subSampleVolume` is hard-coded at 50 mL via `volume_pulse = 0.05`. The schema enforces `min=max=50` so the field is informational only. +- `set.rain` updates are **skipped while `running=true`** — the rain band is only re-evaluated between runs. + +--- + +## Rain-scaled flow prediction + +`parameters.getPredictedFlowRate` linearly scales between `nominalFlowMin` and `flowMax`: + +``` +scale = clamp(avgRain / maxRainRef, 0, 1) +predicted = nominalFlowMin + (flowMax - nominalFlowMin) * scale +``` + +with `avgRain` zeroed after `RAIN_STALE_MS = 2 hours` since the last `set.rain` update. + +`rainAggregator.update` folds the Open-Meteo per-location payload by timestamp, multiplying each hour's `precipitation` by its `precipitation_probability/100` and summing across locations. `avgRain` is the per-location mean of the probability-weighted sum. + +`getModelPrediction` (called inside `_beginRun`) computes the run's expected volume: + +``` +predFlow = max(0, predictedRate · samplingtime) + // falls back to getEffectiveFlow when predictedRate is 0 +``` + +So `predFlow` is total m³ over the run window, and `m3PerPuls = round(predFlow / targetPuls)`. + +--- + +## Flow blending + +`flowTracker` owns three writers: + +| Source | Where it writes | +|:---|:---| +| `data.flow` (operator / parent) | `flow.manual.atequipment` | +| Child `flow.measured.upstream` | `flow.measured.upstream` | +| Child `flow.measured.atequipment` | `flow.measured.atequipment` | +| Child `flow.measured.downstream` | `flow.measured.downstream` | + +`getEffectiveFlow()` blends: + +``` +measured = mean(flow.measured.upstream/atequipment/downstream where present) +manual = flow.manual.atequipment + +effective = (measured + manual) / 2 if both present + = measured if only measured present + = manual if only manual present + = 0 otherwise +``` + +There is no source-priority / preference policy — both sources contribute equally when both are present. Contrast with `rotatingMachine`'s `pressureSelector` which prefers real over virtual children. + +--- + +## Lifecycle — what one event does + +```mermaid +sequenceDiagram + autonumber + participant child as measurement child + participant ops as operator / AQUON + participant monster as monster + participant ft as flowTracker + participant ra as rainAggregator + participant sp as samplingProgram + participant out as Port 0 / 1 + + child->>monster: flow.measured. + monster->>ft: handleMeasuredFlow(eventData) + ops->>monster: set.schedule / cmd.start / data.flow / set.rain + Note over monster: every 1000 ms tick + monster->>ft: getEffectiveFlow() → q + monster->>sp: flowCalc → m3PerTick + monster->>sp: samplingProgram + alt i_start OR now ≥ nextDate + sp->>sp: validateFlowBounds → _beginRun + end + alt running AND stop_time > now + sp->>sp: integrate temp_pulse + sp->>sp: _maybeEmitPulse (cooldown-guarded) + else stop_time elapsed + sp->>sp: _endRun + end + monster->>out: notifyOutputChanged (Port 0/1 delta) +``` + +### Tick interval + +`static tickInterval = 1000` (ms). Set on `nodeClass`. Required by the integrator — `flowCalc` derives `m3PerTick` from the wall-clock delta since the last tick, so the loop must run at a stable cadence. + +### Status interval + +`static statusInterval = 1000` (ms). The status badge re-renders at the same cadence as the tick. + +--- + +## Output ports + +| Port | Carries | Sample shape | +|:---|:---|:---| +| 0 (process) | Delta-compressed state snapshot — `pulse`, `running`, `bucketVol`, `sumPuls`, `m3PerPuls`, `q`, `predFlow`, `timeLeft`, target deltas, rain summary, `nextDate` | `{topic, payload: {running, pulse, bucketVol, ...}}` | +| 1 (telemetry) | InfluxDB line-protocol payload (same fields as Port 0) | `monster,id=cabinet_1 running=true,pulse=false,bucketVol=1.25,m3PerPuls=4,...` | +| 2 (register / control) | `child.register` upward at init | `{topic: 'child.register', payload: {ref, softwareType, config, positionVsParent, distance}}` | + +monster does **not** include a `` segment on flattened measurement keys (it has no per-child key disambiguation — the latest value per position wins). + +See [EVOLV — Telemetry](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Telemetry) for the full InfluxDB layout. + +--- + +## Event sources + +| Source | Where it fires | What it triggers | +|:---|:---|:---| +| Child measurement emitter | `child.measurements.emitter` on `flow.measured.` | `flowTracker.handleMeasuredFlow` | +| Inbound `msg.topic` | Node-RED input wire | `commandRegistry` dispatch → handler | +| `setInterval(tickInterval = 1000)` | `BaseNodeAdapter` | `tick()` → `flowCalc` → `samplingProgram` → `notifyOutputChanged` | +| `setInterval(statusInterval = 1000)` | `BaseNodeAdapter` | Status badge re-render | + +There is no per-state emitter (no `stateChange` / `positionChange`) — monster is purely tick-driven. + +--- + +## Where to start reading + +| If you're changing... | Read first | +|:---|:---| +| Sampling-run init / pulse emission / cooldown | `src/sampling/samplingProgram.js` | +| Bounds + targets math, rain index, flow prediction | `src/parameters/parameters.js` | +| Flow source priority, dead-band, manual blend | `src/flow/flowTracker.js` | +| Open-Meteo aggregation, forecast horizon | `src/rain/rainAggregator.js` | +| Schedule arming, sample-name filtering, daysPerYear | `src/schedule/schedule.js` | +| Topic registration, payload validation, alias deprecation | `src/commands/{index, handlers}.js` | +| Port-0 payload shape, derived fields | `src/io/output.js` | +| Status badge composition | `src/io/statusBadge.js` | +| Node-RED config slice, tick interval | `src/nodeClass.js` `buildDomainConfig` | + +--- + +## 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 | +| [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..fa3170d --- /dev/null +++ b/wiki/Reference-Contracts.md @@ -0,0 +1,204 @@ +# Reference — Contracts + +![code-ref](https://img.shields.io/badge/code--ref-cd185dc-blue) + +> [!NOTE] +> Full topic contract, configuration schema, and child-registration filters for `monster`. Source of truth: `src/commands/index.js`, `src/specificClass.js` `configure()`, and the schema at `generalFunctions/src/configs/monster.json`. +> +> For an intuitive overview, return to the [Home](Home). + +> [!NOTE] +> Pending full node review (2026-05). Content reflects `CONTRACT.md` and current source only. + +--- + +## 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 | +|:---|:---|:---|:---|:---| +| `cmd.start` | `i_start` | `any` (coerced to `Boolean`) | — | Sets `source.i_start`. On the next tick a sampling run begins if `validateFlowBounds` passes. | +| `set.schedule` | `monsternametijden` | array of AQUON rows (`SAMPLE_NAME`, `DESCRIPTION`, `SAMPLED_DATE`, `START_DATE`, `END_DATE`) | — | Stores the schedule and recomputes `nextDate` + `daysPerYear` for the configured `aquonSampleName`. | +| `set.rain` | `rain_data` | per-location rain forecast (Open-Meteo shape) | — | Aggregates hourly precipitation into `sumRain` / `avgRain`. **Skipped while `running=true`.** | +| `data.flow` | `input_q` | `{ value: number, unit: string }` | volumeFlowRate (any unit `convert()` accepts) | Converts to m³/h, pushes into `flow.manual.atequipment`. Blends with measured-child flow in `getEffectiveFlow()`. | +| `set.mode` | `setMode` | any | — | **Reserved.** Handler delegates to `source.setMode()` which is currently undefined; no-op today. | +| `set.model-prediction` | `model_prediction` | any | — | **Reserved.** Handler delegates to `source.setModelPrediction()` which is currently undefined; no-op today. | +| `child.register` | `registerChild` | string (child node id) | — | Register a `measurement` child with this monster. Port 2 wiring does this automatically in normal flows. | + + + +### Mode / source / action allow-lists + +monster has **no allow-list enforcement**. There is no `flowController.handle` equivalent and no `mode.allowedActions` / `mode.allowedSources` config slice. The `set.mode` handler is a placeholder. Compare `rotatingMachine`, which gates every topic through the mode matrix — on monster, every topic dispatches unconditionally. + +--- + +## Data model — `getOutput()` shape + +Composed each tick by `src/io/output.js` `buildOutput()`. Delta-compressed: consumers see only the keys that changed. + +### Flat measurement keys + +For every `(type, variant, position)` stored in MeasurementContainer, `getFlattenedOutput()` emits the three-segment key (note: monster does **not** add a `` segment, unlike `rotatingMachine`): + +| Key | Type | Unit | Notes | +|:---|:---|:---|:---| +| `flow.manual.atequipment` | number | m³/h | Last `data.flow` value (after conversion). | +| `flow.measured.upstream` | number | m³/h | Last measured-child reading at this position. | +| `flow.measured.atequipment` | number | m³/h | Same. | +| `flow.measured.downstream` | number | m³/h | Same. | + +### Scalar keys + + + +| Key | Type | Source | Notes | +|:---|:---|:---|:---| +| `running` | boolean | `m.running` | True between `_beginRun` and `_endRun`. | +| `pulse` | boolean | `m.pulse` | True only on the tick a pulse is emitted; false otherwise. | +| `bucketVol` | number (L) | `m.bucketVol` | Composite volume accumulated this run. | +| `bucketWeight` | number (kg) | `m.bucketWeight` | `bucketVol + emptyWeightBucket`. | +| `sumPuls` | number | `m.sumPuls` | Pulses emitted this run. | +| `pulsesRemaining` | number | `targetPuls - sumPuls` | Clamped to ≥ 0. | +| `m3PerPuls` | number (m³) | `m.m3PerPuls` | Volume per pulse; set in `_beginRun` from `predFlow / targetPuls`. | +| `m3PerPulse` | number (m³) | (alias of `m3PerPuls`) | Both keys emitted; kept for legacy consumers. | +| `m3Total` | number (m³) | `m.m3Total` | Integrated total flow this run. | +| `q` | number (m³/h) | `m.q` | Effective flow (`getEffectiveFlow`). | +| `predFlow` | number (m³) | `m.predFlow` | Predicted total volume over the run window. | +| `predM3PerSec` | number (m³/s) | `m.predM3PerSec` | Predicted average rate during the run. | +| `predictedRateM3h` | number (m³/h) | `params.getPredictedFlowRate` | Rain-scaled flow band between `nominalFlowMin` and `flowMax`. | +| `timePassed` | number (s) | `m.timePassed` | Seconds since `start_time`. | +| `timeLeft` | number (s) | `m.timeLeft` | Seconds remaining until `stop_time`. | +| `pulseFraction` | number | `m.temp_pulse` | Sub-pulse integrator value (0..1+). | +| `flowToNextPulseM3` | number (m³) | derived | Volume left to integrate before the next pulse trigger. | +| `timeToNextPulseSec` | number (s) | derived | ETA to next pulse at current `q`; 0 when `q=0`. | +| `targetVolumeM3` | number (m³) | derived from `targetVolume` (L) | Target composite volume converted to m³. | +| `targetProgressPct` | number | derived | `bucketVol / targetVolume × 100`, 2-dp. | +| `targetDeltaL` | number (L) | derived | Signed L difference vs `targetVolume`. | +| `targetDeltaM3` | number (m³) | derived | Same in m³, 4-dp. | +| `nextDate` | number (epoch ms) | `m.nextDate` | Next scheduled START_DATE for `aquonSampleName`. `null` if never set. | +| `daysPerYear` | number | `m.daysPerYear` | Count of remaining scheduled runs this calendar year. | +| `sumRain` | number | `m.rainAggregator.sumRain` | Probability-weighted hourly precipitation sum. | +| `avgRain` | number | `m.rainAggregator.avgRain` | `sumRain / numberOfLocations`. | +| `nominalFlowMin` | number (m³/h) | config | Lower band for prediction. | +| `flowMax` | number (m³/h) | config | Upper band for prediction. | +| `minVolume` | number (L) | config | Lower bucket bound. | +| `maxVolume` | number (L) | derived | `maxWeight - emptyWeightBucket`. | +| `invalidFlowBounds` | boolean | derived | True when `nominalFlowMin >= flowMax`. | +| `missedSamples` | number | `m.missedSamples` | Pulse attempts blocked by cooldown. | +| `sampleCooldownMs` | number (ms) | derived | Ms remaining on the active cooldown; 0 when none. | +| `minSampleIntervalSec` | number (s) | config | Cooldown window. | + + + +### Status badge + +`buildStatusBadge` in `io/statusBadge.js`: + +| Condition | Badge | Fill | Shape | +|:---|:---|:---|:---| +| `invalidFlowBounds=true` | `Config error: nominalFlowMin (…) >= flowMax (…)` | red | (error preset) | +| `running=true` + `sampleCooldownMs > 0` | `SAMPLING (Ns) · / L` | yellow | ring | +| `running=true` + cooldown clear | `AI: RUNNING · / L` | green | dot | +| idle | `AI: IDLE` | grey | (idle preset) | + +--- + +## Configuration schema — editor form to config keys + +Source of truth: `generalFunctions/src/configs/monster.json` plus `nodeClass.buildDomainConfig`. + +### General (`config.general`) + +| Form field | Config key | Default | Notes | +|:---|:---|:---|:---| +| Name | `general.name` | `"Monster Configuration"` | Free-text. | +| (auto-assigned) | `general.id` | `null` | Node-RED node id. | +| Default unit | `general.unit` | `unitless` | Not used by the sampling program. | +| 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` | `monster` | Constant. | +| (hidden) | `functionality.role` | `samplingCabinet` | Constant. | +| AQUON sample name | `functionality.aquonSampleName` | _unset_ | Forwarded to `source.aquonSampleName` in `extraSetup`. Falls back to `'112100'` in `_initState`. | + +### Asset (`config.asset`) + +| Form field | Config key | Default | Notes | +|:---|:---|:---|:---| +| Asset UUID | `asset.uuid` | `null` | Globally-unique identifier. | +| Geolocation | `asset.geoLocation` | `{x:0, y:0, z:0}` | | +| Supplier | `asset.supplier` | `"Unknown"` | Schema only — not used by the sampling program. | +| Type | `asset.type` | `"sensor"` (enum) | Schema only. | +| SubType | `asset.subType` | `"pressure"` | Schema only; misleading default for a sampling cabinet (flag). | +| Model | `asset.model` | `"Unknown"` | Schema only. | +| Empty bucket weight (kg) | `asset.emptyWeightBucket` | `3` | Used in `bucketWeight = bucketVol + emptyWeightBucket` and in `maxVolume = maxWeight - emptyWeightBucket`. | + +### Constraints (`config.constraints`) + +| Form field | Config key | Default | Range | Notes | +|:---|:---|:---|:---|:---| +| Sampling time (hr) | `constraints.samplingtime` | `0` | ≥ 0 | Run length. Used as `samplingtime · 3600 · 1000` ms for `stop_time`. | +| Sampling period (hr) | `constraints.samplingperiod` | `24` | ≥ 1 | Documented as the fixed composite-collection period; not enforced by `samplingProgram` — the AQUON schedule arms the run. | +| Min volume (L) | `constraints.minVolume` | `5` | ≥ 5 | Used in `targetVolume = minVolume · √(maxVolume / minVolume)`. | +| Max weight (kg) | `constraints.maxWeight` | `23` | ≤ 23 | Bucket-overload bound; `maxVolume = maxWeight - emptyWeightBucket`. | +| Sub-sample volume (mL) | `constraints.subSampleVolume` | `50` | fixed | Schema enforces `min=max=50`. Hard-coded as `volume_pulse = 0.05` in domain. | +| Storage temperature (°C) | `constraints.storageTemperature.min/max` | `{1, 5}` | per-leg | Schema only — informational. | +| Flowmeter present | `constraints.flowmeter` | `true` | bool | ⚠️ In schema but **not** wired in `buildDomainConfig`. Effectively always `true`. | +| Closed system | `constraints.closedSystem` | `false` | bool | Schema only. | +| Intake speed (m/s) | `constraints.intakeSpeed` | `0.3` | ≥ 0 | Schema only — informational. | +| Intake diameter (mm) | `constraints.intakeDiameter` | `12` | ≥ 0 | Schema only — informational. | +| Nominal flow min (m³/h) | `constraints.nominalFlowMin` | `0` | ≥ 0 | Lower bound of rain-driven flow prediction. | +| Flow max (m³/h) | `constraints.flowMax` | `0` | ≥ 0 | Upper bound of rain-driven flow prediction. | +| Max rain reference | `constraints.maxRainRef` | `10` | > 0 | Rain index that maps to `flowMax`. | +| Min sample interval (s) | `constraints.minSampleIntervalSec` | `60` | ≥ 0 | Cooldown guard. | + +> [!WARNING] +> **Default `flowMax = 0` blocks every run.** `validateFlowBounds` requires `0 ≤ nominalFlowMin < flowMax`. Out of the box the bounds are invalid and `_beginRun` never fires. Set `flowMax` to a realistic upper bound before deploying. + +### Unit policy + +monster has **no `requireUnitForTypes` policy** declared in `specificClass`. Conversions happen at the boundary: + +| Quantity | Canonical (internal) | Carried as | Notes | +|:---|:---|:---|:---| +| Flow (`data.flow`) | m³/h | m³/h | `handlers.dataFlow` converts inbound via `convert(value).from(unit).to('m3/h')`. | +| Flow (measured-child) | as supplied | as supplied | `flowTracker.handleMeasuredFlow` defaults to `'m3/h'` when `unit` is missing; **does not convert**. Wire children that emit in m³/h. | +| Volume (`bucketVol`) | L | L | Output also exposes m³ derivations (`targetVolumeM3`, `targetDeltaM3`). | +| Weight | kg | kg | | +| Time | s (timers) / ms (timestamps) | mixed | `timePassed` / `timeLeft` in s, `nextDate` in epoch ms. | + +--- + +## Child registration + +Source: `src/specificClass.js` `_wireMeasurementChild`. The registrar subscribes to all three `flow.measured.` events on the child's measurement emitter as long as the child's `config.asset.type` is `'flow'` or unset. + +| Software type | Filter | Wired to | Side-effect | +|:---|:---|:---|:---| +| `measurement` | `asset.type='flow'` (or missing) | `flowTracker.handleMeasuredFlow` (handles all three positions) | Latches latest measured flow per position; `getEffectiveFlow` blends across positions and with `manualFlow`. | +| `measurement` | `asset.type` anything else | _ignored_ | The branch returns early; no listener is attached. | + +monster has **no position-based filtering**. Unlike `rotatingMachine` (which routes upstream pressure separately from downstream), all three flow positions are wired to the same handler and the latest value per position wins. + +There are **no auto-registered virtual children** (no `dashboard-sim-*` equivalent). Inject simulated flow via `data.flow` instead. + +--- + +## Related pages + +| Page | Why | +|:---|:---| +| [Home](Home) | Intuitive overview | +| [Reference — Architecture](Reference-Architecture) | Code map, sampling-program loop, prediction + cooldown pipeline | +| [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..b2f7667 --- /dev/null +++ b/wiki/Reference-Examples.md @@ -0,0 +1,156 @@ +# Reference — Examples + +![code-ref](https://img.shields.io/badge/code--ref-cd185dc-blue) + +> [!NOTE] +> Every example flow shipped under `nodes/monster/examples/`, plus how to load them, what they show, and the debug recipes that go with them. Live source: `nodes/monster/examples/`. + +> [!NOTE] +> Pending full node review (2026-05). Content reflects `CONTRACT.md`, `examples/README.md`, and current source only. Example flows have not been validated end-to-end on a live Docker instance. + +--- + +## Shipped examples + +| File | Tier | Dependencies | What it shows | +|:---|:---:|:---|:---| +| `basic.flow.json` | 1 | EVOLV only | Quick-start flow — manual flow inject, manual start, seeded `rain_data` and `monsternametijden`, debug taps on the three ports. **Status: pending validation.** | +| `02-integrated-e2e.json` | 2 | EVOLV only | End-to-end integration — measurement child wired in, schedule arming, sampling-run completion. **Status: pending validation.** | + +Per `examples/README.md`, the following files are also referenced but **not currently in this directory** at `cd185dc`: + +| Referenced filename | Status | +|:---|:---| +| `integration.flow.json` | Referenced in README; not present in `examples/` — possibly renamed to `02-integrated-e2e.json`. Flag for review. | +| `edge.flow.json` | Referenced in README; not present. Flag for review. | +| `monster-dashboard.flow.json` | Referenced in README + top-level `README.md`; not present. Flag for review. | +| `monster-api-dashboard.flow.json` | Referenced in README + top-level `README.md`; not present. Flag for review. The README warns that production use requires hardening of placeholder credentials (`__SET_*__`). | + +> [!IMPORTANT] +> Reconcile the example flow inventory: either restore the four referenced flows from history, or update `examples/README.md` + the top-level `README.md` to remove dangling references. Tracked. + +--- + +## 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/monster/examples/basic.flow.json" \ + http://localhost:1880/flows +``` + +--- + +## Example 01 — Basic (`basic.flow.json`) + +Single-cabinet flow with the minimum inputs to drive one sampling run. Pending live validation. + +### Suggested click order after deploy + +1. **Seed the rain forecast.** Click the `set.rain` inject. `sumRain` / `avgRain` populate; `predictedRateM3h` jumps from `nominalFlowMin` to a band-scaled value. (Skip this step and the prediction collapses to `nominalFlowMin`, which is `0` out of the box — the run will still start, but `predFlow` will be 0 and `m3PerPuls` will round to 0; see [Limitations](Reference-Limitations#mperpuls-can-round-to-zero).) +2. **Seed the schedule.** Click the `set.schedule` inject with an AQUON row array containing at least one `START_DATE` in the future for `aquonSampleName=112100` (the default). +3. **Push a manual flow.** Click `data.flow = {value: 240, unit: 'm3/h'}`. +4. **Start.** Click `cmd.start = true`. On the next tick `validateFlowBounds` runs — if `nominalFlowMin < flowMax`, `_beginRun` fires and `running` flips to `true`. +5. **Watch Port 0.** `pulse` blips to `true` for one tick on each integrated-volume threshold cross. `bucketVol` rises in 50 mL steps. `sumPuls` increments. +6. **Stop.** Either wait `samplingtime` hours for `_endRun`, or redeploy. + +> [!IMPORTANT] +> **Screenshot needed.** Editor capture of `basic.flow.json` after step 4. Save as `wiki/_partial-screenshots/monster/basic-running.png`. + +### Try the cooldown guard + +After the first pulse: + +1. Push a higher manual flow (`data.flow = {value: 1200, unit: 'm3/h'}`) so the integrator races past `m3PerPuls` again within `minSampleIntervalSec` (default 60 s). +2. Watch the log: `Sampling too fast. Cooldown active for Ns.` +3. `missedSamples` increments, `pulse` stays `false`, `sampleCooldownMs` ticks down to 0, then the next pulse fires. + +--- + +## Example 02 — Integrated end-to-end (`02-integrated-e2e.json`) + +> [!IMPORTANT] +> **Screenshot needed.** Editor capture of `02-integrated-e2e.json`. Save as `wiki/_partial-screenshots/monster/02-integrated.png`. Replace this callout with the image link. + +One monster + one or more `measurement` flow children. Demonstrates: + +- Auto-registration via Port 2 at deploy (each child's `child.register` reaches the monster). +- Measured + manual flow blend — `getEffectiveFlow` averages both when present. +- Schedule-armed sampling run — `nextDate` triggers `_beginRun` without an `i_start` click. + +Detailed click order: pending review. + +--- + +## Dashboard / API example flows + +The `examples/README.md` references `monster-dashboard.flow.json` (FlowFuse charts for `pulse`, `bucket`, `predFlow`) and `monster-api-dashboard.flow.json` (Open-Meteo + AQUON SFTP + Grafana publish template with placeholder credentials). Neither file is present at `cd185dc`. See the inventory note at the top of this page. + +When restored, the API flow MUST be hardened before production: + +- Replace every `__SET_*__` placeholder with an env-backed secret. +- Pin the Open-Meteo endpoint and Aquon SFTP host to environment variables. +- Route the dashboard publish through `dashboardAPI`, not a hardcoded Grafana URL. + +--- + +## 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 + +| Symptom | First thing to check | Where to look | +|:---|:---|:---| +| `pulse` never fires | Is `running=true`? If not, check `validateFlowBounds` — the `Invalid flow bounds. nominalFlowMin=…, flowMax=…` warn means the default `flowMax=0` is in play. Set `nominalFlowMin < flowMax` in the editor. | `parameters/parameters.js → validateFlowBounds` | +| `pulse` never fires (bounds OK) | Is `m3PerPuls` zero? `_beginRun` rounds `predFlow / targetPuls` to nearest integer m³. Low `predFlow` rounds to 0; integrator divides by 0 → `temp_pulse = Infinity`, the `sumPuls < absMaxPuls` guard then runs out. | `sampling/samplingProgram.js → _beginRun` | +| Pulses arrive too fast / skipped | Cooldown guard active. Inspect `missedSamples` + `sampleCooldownMs`. Look for `Sampling too fast. Cooldown active for Ns.` in the log. | `sampling/samplingProgram.js → _maybeEmitPulse` | +| `q` always zero | Measured-flow child not registered, manual flow not pushed. Watch Port 2 + `data.flow` history. | `flow/flowTracker.js` | +| Measurement child ignored at register | Child's `config.asset.type` is set to something other than `"flow"` (e.g. `"flow-electromagnetic"`). monster only accepts `asset.type = "flow"` or unset. Set `asset.type: "flow"` on the measurement node. | `specificClass.js → _wireMeasurementChild` | +| Measured-flow values look 60× high or wrong unit | `flowTracker.handleMeasuredFlow` does **not** convert: it stores the value as-supplied (defaulting unit to `'m3/h'`). Wire children that already emit m³/h or fix at the source. | `flow/flowTracker.js → handleMeasuredFlow` | +| `nextDate` not arming | `set.schedule` payload didn't include any rows matching `aquonSampleName` with a future `START_DATE`. | `schedule/schedule.js → regNextDate` | +| `sumRain` stays zero with rain input | `set.rain` is ignored while `running=true` — only consumed between runs. Wait for `_endRun` or push the rain payload before `cmd.start`. | `specificClass.js → updateRainData` | +| `data.flow` payloads dropped silently | The handler requires `{value: , unit: }`. Bare numbers or missing `unit` log `data.flow payload must include numeric value and unit.` | `commands/handlers.js → dataFlow` | +| `predictedRateM3h` stays at `nominalFlowMin` | `set.rain` not sent, `lastRainUpdate` is 0, or > 2 hours have passed (`RAIN_STALE_MS`). Re-push the rain payload. | `parameters/parameters.js → getRainIndex` | +| `flowmeter=false` has no effect | `constraints.flowmeter` is in the schema but **not** wired in `buildDomainConfig`. The sampling program always runs in proportional mode. | `src/nodeClass.js → buildDomainConfig` | +| `set.mode` / `set.model-prediction` no-op | Handlers delegate to `source.setMode()` / `source.setModelPrediction()` which don't exist. These topics are reserved for future use. | `commands/handlers.js` | +| Status badge stuck at `Config error: …` | `nominalFlowMin >= flowMax`. Fix the bounds and redeploy. | `io/statusBadge.js` | + +> 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, sampling-program loop, prediction + cooldown pipeline | +| [Reference — Limitations](Reference-Limitations) | Known issues and open questions | +| [EVOLV — Topology Patterns](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topology-Patterns) | Where monster fits in a larger plant | diff --git a/wiki/Reference-Limitations.md b/wiki/Reference-Limitations.md new file mode 100644 index 0000000..275c377 --- /dev/null +++ b/wiki/Reference-Limitations.md @@ -0,0 +1,127 @@ +# Reference — Limitations + +![code-ref](https://img.shields.io/badge/code--ref-cd185dc-blue) + +> [!NOTE] +> What `monster` does not do, current rough edges, and open questions. Open items live in `.agents/improvements/IMPROVEMENTS_BACKLOG.md` in the superproject. + +> [!NOTE] +> Pending full node review (2026-05). Content reflects `CONTRACT.md` and current source only. Known limitations enumerated in `CONTRACT.md` Section 14 are propagated below; additional issues may surface during the planned audit. + +--- + +## When you would not use this node + +| Scenario | Use instead | +|:---|:---| +| Ad-hoc single grab sample | Fire `cmd.start` once and tear it down — monster is overkill for one-off work. A `measurement` child with a one-shot inject is lighter. | +| Generic flow totaliser | `measurement` with the right type/variant, or a dedicated integrator function on the upstream parent. | +| Multi-constituent analyser (NH4, NO3, COD, TSS, …) | monster only emits **pulse + bucket-volume** events — it does **not** model analyte concentrations. The Home page framing ("multi-parameter biological process monitor") describes the **downstream** consumer of the composite sample, not the monster node's outputs. Pending review whether the framing should change or whether analyte fields should be added to Port 0. | +| Time-mode sampling (no flow source) | Not exercised. `constraints.flowmeter = false` exists in the schema but is **not** forwarded in `buildDomainConfig`; the sampling program always runs in flow-proportional mode. | + +--- + +## Known limitations + +### `m3PerPuls` can round to zero + +`_beginRun` computes `m3PerPuls = Math.round(predFlow / targetPuls)`. With low `predFlow` (e.g. an unconfigured `flowMax`, a stale rain forecast, or a very short `samplingtime`) this rounds to 0. The integrator then divides by 0; `temp_pulse` becomes `Infinity`; the first iteration of `_maybeEmitPulse` ramps `sumPuls` up to `absMaxPuls` in one tick and stops. **Symptom: instant bucket fill, then idle.** Workaround: ensure `predFlow ≥ targetPuls` (typical `targetPuls` is 200), e.g. `flowMax ≥ 200 m³` or longer `samplingtime`. Tracked. + +### Default `flowMax = 0` blocks every run + +The schema defaults `nominalFlowMin = 0` and `flowMax = 0`. `validateFlowBounds` requires `min >= 0 && max > 0 && min < max`, so the default config is invalid and the run never starts. The status badge surfaces this as `Config error: nominalFlowMin (0) >= flowMax (0)`. Fix the bounds before deploying. Tracked. + +### `set.rain` is dropped while `running=true` + +`updateRainData` short-circuits when `running` is true — the rain forecast is only consumed between runs. During a long `samplingtime` (e.g. 24 h) the rain forecast goes stale silently. `getRainIndex` does enforce a 2-hour `RAIN_STALE_MS` cap on `avgRain`, but only **outside** a run; during a run the cached `predFlow` is whatever `_beginRun` froze. Open question. + +### `set.mode` and `set.model-prediction` are reserved + +Both handlers delegate to optional methods (`source.setMode()`, `source.setModelPrediction()`) that don't exist on `Monster`. Sending these topics is a no-op. They survive the registry only as forward-compatibility placeholders. Scheduled to either be implemented or removed in a later phase. + +### `constraints.flowmeter` is not wired + +The schema field exists with a `true` default, but `nodeClass.buildDomainConfig` does **not** forward it. The sampling program assumes a flow source unconditionally. Setting `flowmeter=false` in the editor has no runtime effect. Tracked. + +### `subSampleVolume` is hard-coded at 50 mL + +`volume_pulse = 0.05` in `_initSamplingDefaults` is a constant. The schema enforces `min=max=50` so the field is informational only. Bucket-and-target math (`minPuls`, `maxPuls`, `absMaxPuls`, `targetPuls`) all assume 50 mL. Changing this requires a coordinated change across `parameters.js`, the schema, and any consumer that decodes pulse counts back to volume. Tracked. + +### Measured-flow handler does not convert units + +`flowTracker.handleMeasuredFlow` reads `eventData.unit` but only uses it to label the stored measurement — the **value is not converted**. If a measurement child emits in `l/s`, monster stores the raw number and labels it `l/s`, but `getEffectiveFlow` is consumed as if it were m³/h. Wire children that already emit in m³/h, or set `default unit` on the child to `m3/h`. Tracked. + +### Edge test `sampling-guards.edge.test.js` cooldown-guard case fails + +A pre-existing failure documented in `CONTRACT.md` Section 14. The cooldown skip increments `missedSamples` but the assertion expects different timing. Tracked. + +### S88 colour cleanup pending + +The editor node colour in `monster.html` is `#4f8582` (teal). The correct Unit-level S88 colour is `#50a8d9`. The Mermaid diagrams in this wiki already use the correct colour. The editor node tile must be updated separately. Tracked in `.claude/rules/node-red-flow-layout.md` Section 16. + +### No formal FSM + +Unlike `rotatingMachine` (idle → starting → warmingup → operational → …) monster only has the `running` boolean. There is no protected-state guarantee, no abort token, no sequence registry. Cooldown and bounds are the only safety guards. If a future requirement needs e.g. a `pausing` / `paused` state, the sampling program needs to be lifted onto the platform's `state` module. Open question. + +### No multi-parent registration audit + +`childRegistrationUtils.registerChild` runs unconditionally. Whether monster can be safely wired under multiple parents (e.g. one reactor and one settler), and whether teardown ordering is correct, has not been audited. Open question. + +### Multi-constituent reporting absent + +The node's framing in the project memory ("multi-parameter biological process monitor — surrogate for a benchtop analyser") suggests monster reports NH4 / NO3 / COD / TSS / etc. **The current implementation does not.** monster emits volumetric pulse + bucket state only. Lab analyte values are produced by a separate downstream consumer that ingests the physical composite sample collected on the monster's pulses. Either: + +- the framing should be tightened to "composite sampler / sampling-cabinet controller", or +- analyte fields (with timestamps and per-pulse correlations) should be added to Port 0. + +Pending decision-gate review. + +--- + +## Open questions (tracked) + +| Question | Where it lives | +|:---|:---| +| Should `set.rain` updates apply mid-run (best-effort `predFlow` re-compute) or stay run-frozen? | Internal — not yet ticketed | +| Implement or remove `set.mode` + `set.model-prediction` placeholders | Internal | +| Wire `constraints.flowmeter` and exercise the time-mode path | Internal | +| Add unit conversion to `flowTracker.handleMeasuredFlow` | Internal | +| Round-to-zero `m3PerPuls` — clamp to ≥ 1 or refuse to start? | Internal | +| Multi-parent registration safety audit | Internal | +| Add formal FSM (`idle` / `armed` / `running` / `cooldown`)? | Open | +| Add analyte / multi-constituent reporting to Port 0, or rephrase the node's framing | Decision-gate — pending | +| Restore or remove the dangling `monster-dashboard.flow.json` + `monster-api-dashboard.flow.json` references | Internal | + +--- + +## Migration notes + +### From legacy topic aliases + +The pre-canonical topics (`i_start`, `monsternametijden`, `rain_data`, `input_q`, `setMode`, `model_prediction`) are accepted as aliases and emit a one-time deprecation warning. They are scheduled for removal in Phase 7. Update flows to use the canonical names from [Reference — Contracts](Reference-Contracts#topic-contract): + +| Legacy | Canonical | +|:---|:---| +| `i_start` | `cmd.start` | +| `monsternametijden` | `set.schedule` | +| `rain_data` | `set.rain` | +| `input_q` | `data.flow` | +| `setMode` | `set.mode` | +| `model_prediction` | `set.model-prediction` | +| `registerChild` | `child.register` | + +### Pre-2026-05-11 invalid-bounds silent skip + +Before 2026-05-11 `nominalFlowMin` / `flowMax` were not declared in `monster.json`'s `constraints` section. `configUtils` stripped them as unknown keys; bounds collapsed to undefined → NaN → invalid, silently blocking every run. Fixed in the schema; ensure your `monster.json` is at the current commit. + +--- + +## Related pages + +| Page | Why | +|:---|:---| +| [Home](Home) | Intuitive overview | +| [Reference — Contracts](Reference-Contracts) | Topic + config + child filters (alias map at the top) | +| [Reference — Architecture](Reference-Architecture) | Code map, sampling-program loop, prediction + cooldown pipeline | +| [Reference — Examples](Reference-Examples) | Shipped flows + debug recipes | +| [EVOLV — Architecture](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Architecture) | Platform-wide three-tier pattern | diff --git a/wiki/_Sidebar.md b/wiki/_Sidebar.md new file mode 100644 index 0000000..34c9148 --- /dev/null +++ b/wiki/_Sidebar.md @@ -0,0 +1,21 @@ +### monster + +- [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) +- [measurement wiki](https://gitea.wbd-rd.nl/RnD/measurement/wiki/Home) +- [reactor wiki](https://gitea.wbd-rd.nl/RnD/reactor/wiki/Home) +- [settler wiki](https://gitea.wbd-rd.nl/RnD/settler/wiki/Home) +- [dashboardAPI wiki](https://gitea.wbd-rd.nl/RnD/dashboardAPI/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)