diff --git a/package.json b/package.json index c1a1952..6ae1f9d 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,10 @@ "description": "Control module Monsternamekast", "main": "monster.js", "scripts": { - "test": "node --test test/basic/*.test.js test/integration/*.test.js test/edge/*.test.js" + "test": "node --test test/basic/*.test.js test/integration/*.test.js test/edge/*.test.js", + "wiki:contract": "node ../generalFunctions/scripts/wikiGen.js contract ./src/commands/index.js --write ./wiki/Home.md", + "wiki:datamodel": "node ../generalFunctions/scripts/wikiGen.js datamodel ./src/specificClass.js --write ./wiki/Home.md", + "wiki:all": "npm run wiki:contract && npm run wiki:datamodel" }, "repository": { "type": "git", diff --git a/wiki/Home.md b/wiki/Home.md new file mode 100644 index 0000000..6d88ba5 --- /dev/null +++ b/wiki/Home.md @@ -0,0 +1,311 @@ +# monster + +> **Reflects code as of `2a6a0bc` · 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
Process Cell]:::pc + monster[monster
Unit]:::unit + flow_up[measurement
type=flow
position=upstream]:::ctrl + flow_at[measurement
type=flow
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()
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 +``` + +| 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`. + + + +| Canonical topic | Aliases | Payload | Effect | +|---|---|---|---| +| `cmd.start` | `i_start` | `any` | Triggers an action / sequence — not idempotent. | +| `set.schedule` | `monsternametijden` | `any` | Replaces the named state value with the supplied payload. | +| `set.rain` | `rain_data` | `any` | Replaces the named state value with the supplied payload. | +| `data.flow` | `input_q` | `object` | Pushes a value into the node's measurement stream. | +| `set.mode` | `setMode` | `any` | Replaces the named state value with the supplied payload. | +| `set.model-prediction` | `model_prediction` | `any` | Replaces the named state value with the supplied payload. | + + + +## 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
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 +``` + +| 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` | undefined | — | `null` | +| `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` | undefined | — | `null` | +| `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): + +```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 | proportional vs time mode | +| Min sample interval (s) | `constraints.minSampleIntervalSec` | `60` | ≥ 0 | cooldown guard | +| 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` | +| `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` | + +> 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. | `sampling/samplingProgram.js` | +| 4 | Sub-sample volume hard-coded at 50 mL (schema enforces `min=max=50`). | `generalFunctions/src/configs/monster.json` |