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

11 KiB

Reference — Architecture

code-ref

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.

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:

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 — 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.
  • 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

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 — 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 <childId> segment on flattened measurement keys (it has no per-child key disambiguation — the latest value per position wins).

See EVOLV — 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()flowCalcsamplingProgramnotifyOutputChanged
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

Page Why
Home Intuitive overview
Reference — Contracts Topic + config + child filters
Reference — Examples Shipped flows + debug recipes
Reference — Limitations Known issues and open questions
EVOLV — Architecture Platform-wide three-tier pattern