Waterschap Brabantse Delta · R&D-lab · uitleg
diffuser modelleert één fijnbellige beluchtingszone: de gaszijde van een veld
membraan-diffuserelementen op de bodem van een actiefslibreactor. Uit een gemeten luchtdebiet
rekent hij — via een leverancierscurve (SSOTR en drukverlies versus specifieke flux, opgelost bij
de geconfigureerde bottom-coverage) — de zuurstofoverdracht (kg O₂/h) en het drukverlies
(mbar). Hij stuurt geen blower aan: hij rapporteert wat een gegeven luchtdebiet "koopt". De
node is event-driven (geen tick-loop): hij rekent op setter-inputs, niet op een klok. Deze
pagina volgt letterlijk wat de code doet aan de hand van één voorbeeld, een interactief
contract-paneel en een curve-verkenner die de kernfuncties getrouw naspeelt.
RnD/EVOLV (super-repo 74c4089),
submodule diffuser @ 4a9f5b4a50f7 — op 2026-07-17 herverifieerd
identiek aan origin/main (zie verantwoording onderaan).docs/otr-model.html) en op het EVOLV-spoor in HELIX.diffuser is een S88-Equipment Module (leaf, lane L3, kleur
#86bbdd; CLAUDE.md, tabel "S88 classification"). Hij hangt als
child onder een reactor / process cell: bij opstart registreert hij zich via de
Port-2 child.register-handshake bij de parent, en hij registreert zelf
géén children (CONTRACT.md:95-98). Naast oKgo2H is zijn
belangrijkste product de zone-OTR (oZoneOtr), expliciet bedoeld als input voor de
zuurstofbalans van de parent-reactor (CONTRACT.md:57-58). Defaults:
softwareType 'diffuser', role 'Aeration diffuser',
positionVsParent 'atEquipment'
(generalFunctions/src/configs/diffuser.json:63-89).
De node is event-driven — static tickInterval = null: elke setter roept
_recalculate() aan, die bij een echte wijziging notifyOutputChanged()
vuurt; alleen de status-badge wordt elke 1000 ms gepolld
(src/nodeClass.js:7-13; src/specificClass.js:83;
BaseNodeAdapter.js:103-110,134-154). Output verschijnt dus alleen wanneer een input
daadwerkelijk iets verandert — niet op een klok. Er is geen S88-state-machine: de toestand
is een klein vlaggenmodel (idle/warning/alarm,
src/specificClass.js:19-21).
Rode draad door de hele pagina: een zone van 100 elementen
(GVA ELASTOX-R, 0,18 m²/element ⇒ 18 m² membraan), bottom-coverage 15 %,
waterhoogte 5 m, α 0,65, header 500 mbar, zonevolume
1200 m³. Eén commando:
msg.command = { flow: { value: 400, unit: 'm3/h' } }. Alle getallen in de rode draad
zijn met node tegen de echte domainklasse (src/specificClass.js)
en het echte curvebestand (gva-elastox-r.json) doorgerekend; de standalone reimpl in
deze pagina matcht de node exact (testcases in de pagina-JS). Illustratieve aannames zijn expliciet
zo gelabeld.
Het I/O-contract in één oogopslag: kies een verb, stel de waarde 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-draadzone (GVA ELASTOX-R). Elke
bewering draagt een pad:regel-chip. Poortlabels
["process","dbase","parent"]; input-poort label control. Alle verbs lopen
via het canonieke envelope msg.command = { <verb>: {value}, meta }; een
legacy msg.topic-bericht wordt met warn gedropt (CONTRACT.md:6-30).
["process","dbase","parent"],
diffuser.html:36-38; input-label control, :37).
Poort 0 is delta-gecomprimeerd: alleen gewijzigde velden; géén wijziging ⇒ géén
bericht (outputUtils.js:18-67). Sinds "Phase 6" drie i.p.v. vier outputs — de
aparte reactor-controlpoort topic:'OTR' is opgegaan in poort 0 als
oZoneOtr (CONTRACT.md:71-79).| Poort | Label | Belangrijkste keys |
|---|---|---|
| 0 | process |
Delta-gecomprimeerde snapshot uit getOutput()
(src/specificClass.js:220-238): echo iPressure, iMWater, iFlow;
genormaliseerd nFlow (Nm³/h, 2 dec.); curve-resultaat oOtr
(g O₂/Nm³/m), oPLoss (= o_p_total, mbar),
oKgo2H (kg O₂/h); oFlowElement (Nm³/h per element),
oFluxPerM2 (Nm³/(h·m² membraan)), efficiency (0–100),
slope, oZoneOtr; toestand idle (bool),
warning (string[]), alarm (string[]). |
| 1 | dbase |
Zelfde getOutput()-payload via de influxdb-formatter
(measurement = node-naam; tags = geflattende config-metadata, lege/null-tags gedropt;
fields = de gewijzigde waarden) (outputUtils.js:44-113;
CONTRACT.md:62-64). Geen alwaysEmitFields. |
| 2 | parent |
Eenmalig bij startup { topic:'child.register', payload:node.id,
positionVsParent:'atEquipment', distance:null } — dit handshake-pad blijft
platformbreed op legacy msg.topic
(BaseNodeAdapter.js:117-132). |
De parent-reactor staat boven; de diffuser is een
leaf — hij heeft geen eigen kinderen. Registratie loopt omhoog via poort 2 (draad),
telemetrie via poort 0/1 (draad). De zone-OTR (oZoneOtr) op poort 0 is
expliciet bedoeld als voeding voor de zuurstofbalans van de parent. Het gemeten luchtdebiet komt
buiten deze repo vandaan (blower/veld) en bereikt de node als flow-commando.
Klik een stap in de lijst (of gebruik ◀ ▶). Boven zie je waar in de rekenketen je bent; rechts
wat er gebeurt, de waarde van het rode-draadvoorbeeld op dat punt (met node tegen de
echte domainklasse doorgerekend), en het codepad.
De node lost per stap een leverancierscurve op: SSOTR (otr_curve) en
drukverlies (p_curve) versus de specifieke flux J [Nm³/(h·m² membraan)] — de
canonieke x-as van élk curvebestand. Kies een model, schuif de flux-werkpuntmarker. De
otr_curve is geïndexeerd op bottom-coverage-families: bij een
multi-coverage set (alleen aerostrip-phoenix) worden de twee omsluitende
coverage-curves elk op de flux geïnterpoleerd en daarna lineair geblend naar rato van waar
density tussen de keys ligt (_interpolateCurveByDensity,
src/specificClass.js:93-121). Bij een single-coverage set (GVA, Jäger; key
'0') is de coverage-schuif inert — de lookup negeert density. De
p_curve is altijd single-family (density-argument 0, :168). Interpolatie
is lineair, met endpoint-clamping (interpolation.js:116-117,173-200).
Vier code-observaties d.d. 2026-07-17, elk in de code geverifieerd. Ze beschrijven wat de code doet; de duiding is expliciet gelabeld.
getReactorOtr(V) = o_kgo2_h × 1000 × 24 / V — met
o_kgo2_h in kg/h geeft ×1000 gram, dus dimensioneel
g O₂/m³/dag. CONTRACT.md:57-58 en
docs/otr-model.html:190 noemen het "kg O₂/m³/dag": factor
1000 verschil. Rode draad: 893,4 (code) = 0,8936 kg/m³/d (docs).
aanname: docs verouderd, niet de code
(src/specificClass.js:240-244).es = 10^(8,07131 − 1730,63/(233,426+t)) geeft
water-mmHg, terwijl e·100 verderop e als hPa behandelt
(src/specificClass.js:143-147). In de praktijk onschadelijk: beide aanroepen
gebruiken RH = 0 ⇒ e = 0, dus
pd = pPa exact (:42,157).units: { Nm3: { temp: 20, … } } (:266), maar de curvebestanden
declareren stdAir: { temp_C: 0, … } (gva-elastox-r.json:8).
aanname: mogelijk onbedoeld.slope,
minX en maxX alléén van de onderste coverage-curve genomen, niet
geblend — alleen value, minY, maxY blenden
(src/specificClass.js:118-119). aanname: mogelijk
onbedoeld.n_flow = (o_kg/n_kg)·flow met
o_kg = ρ(atm+header, 0, 20) en n_kg = ρ(1013,25, 0, 20). Bij header
500 mbar: ratio 1,493 ⇒ 400 m³/h → 597,38 Nm³/h.
Daarom verandert headerPressure het antwoord bij gelijk bedrijfsdebiet
(:156-159; docs/otr-model.html:83).yAxisBasis: "ssotr-g-per-Nm3-per-m"
(gva-elastox-r.json:13) — g O₂/Nm³ per meter inblaasdiepte. Daarom klopt
de vermenigvuldiging met waterhoogte h in
o_kgo2_h = OTR·n_flow·h·α [g→kg] (:175-176).gva-elastox-r.json:_meta). Config > curve-_meta > fallback
0,18 m² (:34-40).Er is geen S88-state-machine. De toestand is een klein vlaggenmodel:
idle is true bij constructie en zodra i_flow ≤ 0 — dan worden
alle afgeleide outputs genuld en warning/alarm gewist, in plaats van buiten de curve te
extrapoleren (src/specificClass.js:19,70-78). warning en
alarm bewaken de specifieke flux tegen het meetbereik van de ΔP-curve met hysterese:
warning ±2 %, alarm ±10 % (:20-21,189-202). Badge-prioriteit:
alarm (rood) > warning (geel, "⚠ …") > idle (grijs) > groen met tekst
"<oKgo2H> kg o2 / h" (:213-218). Alle input loopt via het canonieke
verb-envelope; de zes verbs (flow, density, waterHeight, headerPressure, elements,
alfaFactor) dispatchen 1-op-1 naar de setters (src/commands/index.js:15-68;
handlers.js:14-19).
diffuser.html:9-33). Volledige
defaults/enums: generalFunctions/src/configs/diffuser.json. ⚠️
buildDomainConfig leest ook i_pressure, i_local_atm_pressure,
i_water_density, membraneAreaPerElement (src/nodeClass.js:30-37), maar géén
van deze vier heeft een editorveld — via de editor vallen ze altijd op de fallback;
headerPressure is runtime alleen via het gelijknamige commando te zetten.| Veld | Default | Betekenis |
|---|---|---|
name | "" (verplicht) | Zonenaam; label wordt name_number, anders model of 'diffuser' (diffuser.html:40-43). |
i_elements | 1 | Aantal diffuserelementen (afgerond, > 0 via _posInt). |
i_diff_density | 15 | Bottom coverage [%] — fractie van de tankbodem bezet door membraan; sleutel van de curve-familie (diffuser.html:89-93). |
i_m_water | 0 | Waterhoogte boven de diffusers [m] (herberekent statische druk). |
alfaf | 0.7 | Alfa-factor (afvalwater vs. schoon water). |
i_zone_volume | 0 (optioneel) | Zonevolume [m³] voor oZoneOtr; 0 ⇒ oZoneOtr = 0. |
model (asset) | 'gva-elastox-r' | Curve-model via de asset-cascade; fallback-keten asset.model → model → 'gva-elastox-r', ook bij onbekend model (src/specificClass.js:246-259). |
processOutputFormat / dbaseOutputFormat | 'process' / 'influxdb' | Formatter poort 0 / poort 1 (diffuser.html:17-18,109-125). |
Alle punten zijn code-observaties d.d. 2026-07-17 (statische analyse, niet in runtime
geverifieerd). Elk punt draagt een pad:regel-verwijzing; duiding is gelabeld.
oZoneOtr — code levert g O₂/m³/dag, contract/docs
zeggen kg (factor 1000; src/specificClass.js:240-244;
CONTRACT.md:57-58). aanname: docs verouderd.units.Nm3.temp: 20, :266) vs. curve-_meta
stdAir.temp_C: 0 (gva-elastox-r.json:8).
aanname: mogelijk onbedoeld.:143-147); onschadelijk want beide aanroepen gebruiken RH = 0
(:42,157).slope/minX/maxX alleen van de
onderste coverage-curve, niet geblend (:118-119).
aanname: mogelijk onbedoeld.httpAdmin-GET's in
diffuser.js:14-30 (/diffuser/menu.js,
/diffuser/configData.js) hebben géén
RED.auth.needsPermission('diffuser.read')-guard, terwijl de repo-securityregel dat
voorschrijft (.claude/rules/security.md).test/specificClass.test.js:23 en examples/basic.flow.json gebruiken
nog density: 2.4 (oude "elements/m²"-semantiek) i.p.v. de bottom-coverage-default
15; onschadelijk voor de single-key GVA-curve, maar semantisch gedrift.graph.js (152 regels, © 2023) wordt nergens
geïmporteerd; diffuser_class.js is een 1-regel back-compat shim. Twee testbestanden
zijn placeholders (assert.equal(true, true)); geen test/_output-manifest.md
(vereist door .claude/rules/output-coverage.md bij output-wijzigingen).docs/otr-model.html:229). De
GVA-defaultcurve is bovendien een point-estimate met placeholder-membraanoppervlak
(gva-elastox-r.json:_meta).CONTRACT.md:84-88 noemt het promoveren van
oOtr/oZoneOtr naar typed measurements via
MeasurementContainer/ChildRouter; vandaag emit het domein alleen
'output-changed'.git.wbd-rd.nl/RnD/diffuser/issues) is niet geraadpleegd voor deze analyse
(aanname: niet hier verifieerbaar).docs/otr-model.html, docs/functional.html; contract in
CONTRACT.md; curvebestanden in
generalFunctions/datasets/assetData/curves/ (RnD/diffuser,
RnD/generalFunctions).RnD/EVOLV
(super-repo 74c4089, 2026-07-03) — submodule diffuser @
4a9f5b4a50f7, plus het feitendossier van deze analyse. Analysedatum 2026-07-17.git fetch
gedraaid: de submodule-tip diffuser origin/main is
4a9f5b4a50f7 — identiek aan de geanalyseerde werkkopie. Geen open fix-branches voor
deze node meegenomen.pad:regel-verwijzing. De curve-lookup, dichtheids-normalisatie en OTR zijn
vooraf met node tegen de echte domainklasse
(src/specificClass.js) en de echte curvebestanden doorgerekend — de standalone reimpl
in de pagina-JS matcht de node exact (testgevallen als commentaar in de JS); illustratieve
aannames zijn expliciet zo gelabeld.