# monster ![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) 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. > [!NOTE] > Pending full node review (2026-05). Content reflects `CONTRACT.md` and current source only. --- ## 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 parent[plant parent
Process Cell]:::pc 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 (pulse / bucketVol).-> parent classDef pc fill:#0c99d9,color:#fff classDef unit fill:#50a8d9,color:#000 classDef ctrl fill:#a9daee,color:#000 ``` 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. --- ## Try it — 3-minute demo Import the basic example flow, deploy, and drive the sampler through a single run. ```bash curl -X POST -H 'Content-Type: application/json' \ --data @nodes/monster/examples/basic.flow.json \ http://localhost:1880/flow ``` What to click after deploy (the inject buttons map to the topics in [Reference — Contracts](Reference-Contracts#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`. > [!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`. --- ## 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. | There is no `query.*` topic on monster (unlike `rotatingMachine`'s `query.curves` / `query.cog`). All state is on Port 0. --- ## What you'll see come out Sample Port 0 message (mid-run snapshot, delta-compressed): ```json { "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 } } ``` | 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`. | Full Port-0 key list: see [Reference — Contracts — Data model](Reference-Contracts#data-model--getoutput-shape). 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. --- ## Status badge | 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 | --- ## Need more? | 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 | [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)