- Banner hash updated to8540328(was stale2a6a0bc). - Section 9: flag constraints.flowmeter not wired in buildDomainConfig (effectively always true at runtime); add nominalFlowMin / flowMax / maxRainRef rows that were missing from the config table. - Section 12: add two new debug recipes — measurement child ignored (assetType must be "flow", not "flow-electromagnetic") and flowmeter=false has no runtime effect. - Section 14 #3: expand flowmeter limitation to call out buildDomainConfig gap. - Section 14 #5: add S88 colour cleanup note (#4f8582 teal → #50a8d9 Unit). - AUTOGEN markers untouched; npm run wiki:all re-ran cleanly before commit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
319 lines
15 KiB
Markdown
319 lines
15 KiB
Markdown
# 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.
|
|
|
|
## 1. What this node is
|
|
|
|
**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.
|
|
|
|
## 2. Position in the platform
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
parent[plant parent<br/>Process Cell]:::pc
|
|
monster[monster<br/>Unit]:::unit
|
|
flow_up[measurement<br/>type=flow<br/>position=upstream]:::ctrl
|
|
flow_at[measurement<br/>type=flow<br/>position=atequipment]:::ctrl
|
|
op[(operator / AQUON)]
|
|
weather[(Open-Meteo)]
|
|
|
|
flow_up -->|flow.measured.upstream| monster
|
|
flow_at -->|flow.measured.atequipment| monster
|
|
op -->|set.schedule / data.flow / cmd.start| monster
|
|
weather -->|set.rain| monster
|
|
monster -->|child.register| parent
|
|
monster -.evt.output.-> 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`.
|
|
|
|
## 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. |
|
|
|
|
## 4. Code map
|
|
|
|
```mermaid
|
|
flowchart TB
|
|
subgraph nodeRED["nodeClass.js — adapter (BaseNodeAdapter)"]
|
|
nc["buildDomainConfig()<br/>static DomainClass = Monster<br/>static commands"]
|
|
end
|
|
subgraph domain["specificClass.js — orchestrator (BaseDomain)"]
|
|
sc["Monster.configure()<br/>wires concerns, ChildRouter rules<br/>tick() → flowCalc → samplingProgram"]
|
|
end
|
|
subgraph concerns["src/ concern modules"]
|
|
parameters["parameters/<br/>bounds + targets + rain index"]
|
|
flow["flow/<br/>FlowTracker (measured + manual)"]
|
|
rain["rain/<br/>RainAggregator (sum + avg)"]
|
|
schedule["schedule/<br/>AQUON next-date parser"]
|
|
sampling["sampling/<br/>samplingProgram + flowCalc"]
|
|
io["io/<br/>output + statusBadge"]
|
|
commands["commands/<br/>topic registry + handlers"]
|
|
end
|
|
nc --> sc
|
|
sc --> parameters
|
|
sc --> flow
|
|
sc --> rain
|
|
sc --> schedule
|
|
sc --> sampling
|
|
sc --> io
|
|
nc --> commands
|
|
```
|
|
|
|
| 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. |
|
|
|
|
## 5. Topic contract
|
|
|
|
> **Auto-generated** from `src/commands/index.js`. Do NOT hand-edit between the markers. Re-run `npm run wiki:contract`.
|
|
|
|
<!-- BEGIN AUTOGEN: topic-contract -->
|
|
|
|
| Canonical topic | Aliases | Payload | 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. |
|
|
|
|
<!-- END AUTOGEN: topic-contract -->
|
|
|
|
## 6. Child registration
|
|
|
|
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<br/>asset.type=flow<br/>position=upstream"]:::ctrl
|
|
m_at["measurement<br/>asset.type=flow<br/>position=atequipment"]:::ctrl
|
|
m_dn["measurement<br/>asset.type=flow<br/>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
|
|
```
|
|
|
|
| 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.<position>
|
|
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.
|
|
|
|
<!-- BEGIN AUTOGEN: data-model -->
|
|
|
|
| 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` |
|
|
|
|
<!-- END AUTOGEN: data-model -->
|
|
|
|
**Concrete sample** (mid-run snapshot):
|
|
|
|
```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
|
|
}
|
|
```
|
|
|
|
Concrete samples must come from a known-good test run. Regenerate when concern modules change shape.
|
|
|
|
## 9. Configuration — editor form ↔ config keys
|
|
|
|
```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
|
|
```
|
|
|
|
| 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
|
|
|
|
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.
|
|
|
|
## 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 |
|
|
|
|
One screenshot per tier where helpful. PNG ≤ 200 KB under `wiki/_partial-screenshots/monster/`.
|
|
|
|
## 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 |
|