Waterschap Brabantse Delta · R&D-lab · uitleg
monster is de EVOLV-besturingsnode voor een monsternamekast op een RWZI.
Het is een pulsteller, geen analyzer: de node telt pulsen — elke puls één
deelmonster van 0,05 L — over een bemonsteringsvenster, en bepaalt de pulscadans
debietproportioneel uit gemeten/handmatig debiet, regenverwachting (Open-Meteo) en
het AQUON-rooster, met bewaking van emmer-constraints (volume, gewicht, cooldown). Hij
analyseert geen constituenten (NH4/NO3/COD/TSS) — daarvoor koppel je een aparte
analyzer-node. De node is tick-gedreven (elke 1000 ms) en levert proces-,
Influx- en registratie-output op drie poorten. Deze pagina volgt letterlijk wat de code doet
aan de hand van één voorbeeldrun en een interactieve figuur die de pulsintegrator getrouw
naspeelt.
RnD/EVOLV, submodule monster @
c4fb9baaf371 (identiek aan origin/main op de peildatum; zie de
verantwoording onderaan).monster stuurt debietproportionele bemonstering: hoe meer water passeert, hoe
sneller de pulsen elkaar opvolgen, zodat het verzamelmonster het werkelijke debiet
representeert (nodes/monster/README.md:5-9). In de S88-classificatie is de node
een Unit (lane L4, groepskleur #50a8d9;
nodes/monster/CLAUDE.md). Als child registreert hij zich bij zijn parent
via de Port-2 child.register-handshake; als parent aanvaardt hij zelf
alleen measurement-kinderen met asset.type = 'flow'
(src/specificClass.js:37,99-108). Functionele rol
samplingCabinet, softwareType monster
(configs/monster.json).
Pulsteller, geen analyse. README, CLAUDE.md en CONTRACT.md leggen de
scope vast als "pulse counter — measures sampled volumes and pulse cadence; does not
analyze constituents". Er zitten dan ook geen analyseparameters in code of config — alleen
AQUON-monsterpuntcodes als rooster-filter. Let op: het klasse-commentaar
src/specificClass.js:3 noemt de node nog "multi-parameter biological process
monitoring" — dat is verouderd/onjuist en spreekt de vastgelegde scope tegen; de code
zelf telt uitsluitend pulsen (code-observatie 2026-07-17).
Tick-gedreven. De adapter zet static tickInterval = 1000:
elke seconde draait Monster.tick() (debiet ophalen → volume integreren →
eventueel een puls). Output verschijnt dus op het seconde-ritme, niet event-gedreven. Bij een
gezet tickInterval heeft het interne notifyOutputChanged()-event
geen listener — de outputs komen uitsluitend uit de interval-callback en de emit is
hier effectief een no-op (zelfde patroon als measurement;
src/specificClass.js:152-158; BaseNodeAdapter.js:136-153).
Rode draad door de hele pagina: een monsternamekast met
minVolume 5 L, maxWeight 23 kg,
emptyWeightBucket 3 → maxVolume 20 L;
targetVolume = 5·√(20/5) = √(min·max) = 10 L → targetPuls = round(10/0,05) =
200; absMaxPuls = round(55/0,05) = 1100. Alle getallen zijn met node
tegen parameters.js + samplingProgram.js doorgerekend
(testgevallen als commentaar in de pagina-JS).
Het I/O-contract in één oogopslag: kies een verb, stel de velden in,
kopieer het exacte injecteerbare msg, en klik Injecteer ▸ om te zien wat
er uit de drie poorten komt — met live doorgerekende waarden op de rode-draadkast
(targetVolume 10 L / targetPuls 200). Elke bewering draagt een
pad:regel-chip. Poortlabels ["process","dbase","parent"].
["process","dbase","parent"],
monster.html:53-56). Poort 0/1 zijn delta-gecomprimeerd: alleen
gewijzigde velden; geen wijziging → geen bericht (outputUtils.js:17-41).
Object-waarden (behalve Date) worden ge-JSON-stringified.| Poort | Label | Belangrijkste keys / gedrag |
|---|---|---|
| 0 | process |
Delta-snapshot (src/io/output.js:30-65) bovenop de geflattende
measurements: pulse, running,
bucketVol, bucketWeight, sumPuls,
m3PerPuls (+ alias m3PerPulse), m3Total,
q, predFlow, predM3PerSec,
predictedRateM3h, pulsesRemaining,
pulseFraction, flowToNextPulseM3,
timeToNextPulseSec, targetVolumeM3,
targetProgressPct, targetDeltaL/targetDeltaM3,
missedSamples, sampleCooldownMs,
minSampleIntervalSec, nominalFlowMin,
flowMax, invalidFlowBounds, maxVolume,
minVolume, nextDate, daysPerYear,
timePassed, timeLeft, sumRain,
avgRain. |
| 1 | dbase |
Zelfde snapshot via de influxdb-formatter, met config-tags
(id, softwareType, role, positionVsParent, uuid, tagcode, geoLocation,
category, type, model, unit; lege/undefined gedropt). Eigen delta-state per
format (outputUtils.js:87-133). |
| 2 | parent |
Eenmalig bij opstart (na 100 ms): { topic:'child.register',
payload:node.id, positionVsParent, distance }
(BaseNodeAdapter.js:117-131); daarna ongebruikt. |
De parent staat boven (registratie via draad,
poort 2); measurement-kinderen (flow) staan onder en komen als
in-process events binnen. De omringende e2e-flow (gestippeld, buiten de node
zelf) levert de externe bronnen — regen (Open-Meteo), rooster (AQUON SFTP-CSV) en debiet
(PLC/measurement) — en verwerkt de node-output: het pulscommando naar de PLC plus
telemetrie naar InfluxDB / Z-Info. Onderscheid dus wat de node doet (midden)
van wat de keten doet (gestippeld).
De node zelf doet: verbs valideren, debiet
blenden, regen aggregeren, het rooster lezen, de pulsintegrator draaien en de drie poorten
vullen. De keten (gestippeld) haalt de regen op bij Open-Meteo, het rooster via AQUON
SFTP, het debiet uit een PLC/measurement, en zet de output door naar de PLC (puls) en naar
InfluxDB/Z-Info (telemetrie/rapportage). Die externe knopen zitten alleen in de
voorbeeldflow examples/02-integrated-e2e.json, niet in de node
(examples/README.md:12-17).
Klik een stap in de lijst (of gebruik ◀ ▶). Boven zie je waar in de tick-keten je bent;
rechts wat er gebeurt, de waarde van het rode-draadvoorbeeld op dat punt (met node
tegen parameters.js/samplingProgram.js doorgerekend) en het codepad.
Stap 7 draagt het deling-door-nul-randgeval — het kernpunt van deze node.
Doelvolume = meetkundig gemiddelde. De grenzen komen uit
src/parameters/parameters.js:10-17:
maxVolume = maxWeight − emptyWeightBucket (kg ≈ L). Het doelvolume is
niet het rekenkundige, maar het meetkundig gemiddelde van min en max — het ligt
daardoor logaritmisch tussen de grenzen en overweegt de kleine emmer niet:
| Symbool | Betekenis | Eenheid | Herkomst |
|---|---|---|---|
doelvolume emmer einde run (targetVolume) — bij 5–20 L = 10 L | L | parameters.js:15 | |
minimaal emmervolume (minVolume, default 5) | L | config constraints.minVolume | |
maxWeight − emptyWeightBucket — bij 23−3 = 20 L | L | parameters.js:11 | |
pulsdoel (targetPuls) — round(10/0,05) = 200; absMaxPuls = round(55/0,05) = 1100 | — | parameters.js:14,16 |
Pulscadans (met de deling-door-nul-val). Bij run-start wordt het volume-per-puls
vastgezet uit de voorspelling; daarna integreert elke tick het passerende volume en
vuurt een puls zodra de integrator 1 bereikt (samplingProgram.js:41,105):
| Symbool | Betekenis | Eenheid | Herkomst |
|---|---|---|---|
volume per puls (m3PerPuls), geheel afgerond — wordt 0 zodra < 0,5· | m³ | samplingProgram.js:41 | |
voorspeld venstervolume (predFlow) = max(0, debiet · samplingHours) | m³ | samplingProgram.js:14-21 | |
pulsintegrator (temp_pulse); bij een puls: p ← p − 1 | — | samplingProgram.js:85,105 | |
| effectief debiet (blend gemeten/handmatig) | m³/h | flowTracker.js:49-56 | |
| wall-clock sinds vorige tick (eerste tick: 0) | s | samplingProgram.js:23-27 |
Debietproportioneel, interactief. m3PerPuls is bij
run-start vastgezet uit de voorspelde inflow; tijdens de run drijft het werkelijke
debiet de integrator. Hoger werkelijk debiet ⇒ snellere pulsen; loopt het de cooldown voorbij,
dan worden pulsen geblokkeerd en tellen ze als missedSamples. Zet
samplingtime op 0 (de editor-default!) en predFlow wordt 0 → m3PerPuls
= 0 → de integrator deelt door nul.
minVolume vast op 5 L. De trap is
bucketVol(t) — elke puls +0,05 L. De targetVolume-lijn
(√(min·max)) en het maxVolume-plafond staan ingetekend. Werkelijk > voorspeld
debiet ⇒ overschot & missedSamples; samplingtime = 0
⇒ m3PerPuls = 0 ⇒ deling door nul.Alle punten zijn code-observaties d.d. 2026-07-17 (statisch, niet in runtime
geverifieerd), elk met pad:regel. Duidingen zijn expliciet als
aanname gemarkeerd.
m3PerPuls = round(predFlow / targetPuls) wordt 0 zodra
predFlow < 0,5·targetPuls (= < 100 m³ bij targetPuls 200) —
bijvoorbeeld bij samplingtime = 0, de editor-default. Daarna is
temp_pulse += m3PerTick / m3PerPuls een deling door 0 →
Infinity; er is geen guard. Bij Infinity vuurt de puls
elke cooldown-periode tot absMaxPuls (1100).
aanname: of dit live optreedt is onzeker
(samplingProgram.js:41,105; brief §8.1).targetVolume = minVolume·√(maxVolume/minVolume) = √(min·max),
niet het rekenkundige gemiddelde. Bij 5–20 L geeft dat 10 L (rekenkundig
zou 12,5 L zijn) — het doel ligt logaritmisch tussen de grenzen.notifyOutputChanged() emit 'output-changed', maar
bij een gezet tickInterval luistert niemand daarnaar — output komt uit de
interval-callback. De emit is een no-op (zoals measurement).Math.round op m³/puls stapt per 1 m³ en kan bij lage
volumes fors afwijken van targetPuls. In de figuur: predFlow 144 geeft
round(0,72) = m3PerPuls 1 i.p.v. 0,72 → grovere puls → ~144 i.p.v.
200 pulsen → onderschot (7,2 L vs. targetVolume 10 L).samplingperiod, subSampleVolume,
storageTemperature, flowmeter, closedSystem,
intakeSpeed, intakeDiameter staan in het schema maar worden
nergens gelezen (grep bevestigd). Ook predFactor = 0.7 en
currHour gezet-maar-ongebruikt; minPuls/maxPuls
berekend maar niet sturend (specificClass.js:50,56).·3600·1000) — de schema-beschrijving is fout.
maxweight: editor-default 22 (→ maxVolume 19) vs. schema/adapter 23
(→ 20). aanname: mogelijk onbedoeld.!running; een run gebruikt de
regenindex van vóór de start. lastRainUpdate wordt wél ververst, dus de
staleness-guard (>2 u) blijft "vers" ook al is de aggregatie oud.nextDate is een Date-object (init, +1 jaar)
óf een epoch-getal (uit het rooster). Vóór de eerste schedule is het
undefined → Date.now() >= undefined is false →
alleen handmatige start mogelijk.updateMonsternametijden checkt alleen value[0] op de
vijf velden; een array met één geldige eerste rij en corrupte rest wordt
geaccepteerd./monster/menu.js en /monster/configData.js hebben
geen RED.auth.needsPermission, terwijl .claude/rules/security.md
dat voorschrijft (referentie edgeIO).test/_output-manifest.md en geen dedicated
output-*.test.js met populated/degraded-states per Port-0/1-key
(.claude/rules/output-coverage.md niet ingevuld voor deze node).Er is geen formele state- of mode-machine: de toestand is een set booleans/getallen
op het domein — running, pulse, i_start (startgate) en
invalidFlowBounds (specificClass.js:63-96). Run-levenscyclus
(samplingProgram.js:92-111): start zodra i_start gezet is óf
now ≥ nextDate én !running, mits validateFlowBounds
slaagt (min≥0, max>0, min<max, eindig); _beginRun zet
m3PerPuls + stop_time = now + samplingtime·3600·1000; de tick
integreert; na stop_time reset _endRun alles.
De statusbadge (io/statusBadge.js:13-26): rood ring bij
invalidFlowBounds, geel ring SAMPLING (Ns) + niveau bij actieve
cooldown, groen dot AI: RUNNING bij normaal bemonsteren, grijs AI: IDLE
bij idle. De verbs mode en modelPrediction zijn "reserved":
setMode bestaat niet (no-op via typeof-guard), terwijl
setModelPrediction wél predFlow zet (handlers.js:39-45).
monster.html:12-50). Volledig schema:
nodes/generalFunctions/src/configs/monster.json. Vast in code (niet
configureerbaar): volume_pulse = 0,05 L per puls en
cap_volume = 55 L (specificClass.js:78,82).| Veld | Default (editor) | Betekenis |
|---|---|---|
samplingtime | 0 | lengte bemonsteringsvenster in uren (default 0 = deling-door-nul-val) |
minvolume | 5 | minimaal emmervolume (L) |
maxweight | 22 | maximaal emmergewicht (kg); schema/adapter = 23 — zie drift |
emptyWeightBucket | 3 | leeggewicht emmer (kg) |
nominalFlowMin / flowMax | 0 / 0 | onder-/bovengrens voorspellingsband (m³/h) |
maxRainRef | 10 | regenindex (mm) die op flowMax mapt |
minSampleIntervalSec | 60 | pulscooldown (s); snellere pulsen → missedSamples |
aquon_sample_name | "" | AQUON-monsterpuntcode (dropdown menu 'aquon'); filtert het rooster (default in code '112100') |
De AQUON-dropdown wordt gevuld via het MenuManager-menu 'aquon'
(monster.js:19) uit de namespaces monsterSamples (codes +
omschrijvingen) en monsterSpecs; bij selectie vult de editor de constraint-velden
voor uit de spec (merge defaults ← per-sample), maar de velden blijven bewerkbaar
(monster.html:94-165). In de huidige dataset zijn alle bySample-specs
identiek aan de defaults.
docs/contract.html,
docs/functional.html, docs/sampling.html en
nodes/monster/CONTRACT.md.examples/basic.flow.json en
examples/02-integrated-e2e.json ("Not yet validated against live Node-RED",
examples/README.md:12-17).RnD/EVOLV (super-repo 74c4089, 2026-07-03) — submodule
monster geanalyseerd @ c4fb9baaf371, plus het feitendossier van
deze analyse. Analysedatum 2026-07-17.git fetch
gedraaid: de submodule-tip origin/main = c4fb9baaf371 — identiek
aan de geanalyseerde werkkopie. De aandachtspunten hierboven beschrijven die actuele
status.pad:regel-verwijzing. De interactieve figuur en de console zijn
getrouwe herimplementaties, vooraf met node tegen parameters.js en
samplingProgram.js doorgerekend (testgevallen als commentaar in de pagina-JS);
aannames zijn expliciet zo gelabeld.