Files
monster/wiki/Reference-Architecture.md
znetsixe 76951f104d 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) <noreply@anthropic.com>
2026-05-19 09:42:13 +02:00

246 lines
11 KiB
Markdown

# Reference &mdash; 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 &mdash; 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&#40;&#41; — every 1000 ms]
tick --> q[q = flowTracker.getEffectiveFlow&#40;&#41;]
q --> fc[flowCalc&#40;&#41;<br/>m3PerTick = q/3600 * dt]
fc --> sp[samplingProgram&#40;&#41;]
sp --> trig{i_start OR<br/>now ≥ nextDate?<br/>AND NOT running}
trig -- yes --> vb{validateFlowBounds?}
vb -- no --> done[return — running stays false]
vb -- yes --> begin[_beginRun<br/>m3PerPuls = predFlow / targetPuls<br/>stop_time = now + samplingtime·h]
trig -- no --> active{stop_time &gt; now?}
begin --> active
active -- yes --> integ[temp_pulse += m3PerTick / m3PerPuls<br/>m3Total += m3PerTick]
integ --> pulse[_maybeEmitPulse]
pulse --> emit{temp_pulse ≥ 1<br/>AND sumPuls &lt; absMaxPuls?}
emit -- no --> notify[notifyOutputChanged]
emit -- yes --> cd{cooldown<br/>blocked?}
cd -- yes --> miss[missedSamples++<br/>warn one-shot]
miss --> notify
cd -- no --> fire[temp_pulse -= 1<br/>pulse = true<br/>sumPuls++<br/>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 &mdash; `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 &mdash; 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`** &mdash; 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 &mdash; both sources contribute equally when both are present. Contrast with `rotatingMachine`'s `pressureSelector` which prefers real over virtual children.
---
## Lifecycle &mdash; 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.<position>
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 &mdash; `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 &mdash; `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 `<childId>` segment on flattened measurement keys (it has no per-child key disambiguation &mdash; the latest value per position wins).
See [EVOLV &mdash; 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.<upstream/atequipment/downstream>` | `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`) &mdash; 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 &mdash; Contracts](Reference-Contracts) | Topic + config + child filters |
| [Reference &mdash; Examples](Reference-Examples) | Shipped flows + debug recipes |
| [Reference &mdash; Limitations](Reference-Limitations) | Known issues and open questions |
| [EVOLV &mdash; Architecture](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Architecture) | Platform-wide three-tier pattern |