Waterschap Brabantse Delta · R&D-lab · uitleg

EVOLV · generalFunctions — de gedeelde ruggengraat onder alle nodes

eigenaar R&D-lab laatst getest 2026-07-17 versie 2.0 concept
generalFunctions is géén Node-RED-procesnode met een input→output-reis, maar de gedeelde bibliotheek waarop elke andere EVOLV-node leunt. De eigen CONTRACT.md zegt het letterlijk: de publieke API is "wat require('generalFunctions') exporteert" (CONTRACT.md:1-5), en de superproject-regel voegt toe: "generalFunctions is shared by ALL 13 nodes. Any change here can break any node" (.claude/rules/general-functions.md:3-4). Er is dus geen 9-staps-pijplijn om te volgen; deze pagina toont in plaats daarvan een kaart-grid per hoofdmodule plus twee gerichte, node-geteste demo's van de contracten die álle nodes delen: de meet-adressering en het poort 0/1/2-contract. Omdat dit de meest- opgezochte node is, staat correctheid van die gedeelde contracten voorop.

De ruggengraat in kaarten

Eén kaart per hoofdmodule van de barrel (index.js:65-115 — het contractuele toegangspunt; "if this file and the barrel disagree, the barrel wins", CONTRACT.md:13-16,116-117). Feitelijke afnemers, geteld via imports in nodes/*/src: coresync, dashboardAPI, diffuser, edgeIO, machineGroupControl, measurement, monster, pumpingStation, reactor, rotatingMachine, settler, valve, valveGroupControl (13 nodes; rotatingMachine met 14 importerende bestanden de grootste afnemer). Elke kaart draagt een pad:regel-verwijzing.

BaseNodeAdapter
platform · nodeClass-lijm
Gedeelde nodeClass-scaffolding: bouwt de config via ConfigManager.buildConfig, instantieert de static DomainClass, maakt een OutputUtils (met static alwaysEmitFields) en de command-registry, bedraadt de tick- óf event-gedreven outputloop, drijft de StatusUpdater en plant de child-registratie op poort 2. Injecteert impliciet een query.units-commando.
src/nodered/BaseNodeAdapter.js:51-213
BaseDomain
platform · specificClass-lijm
Gedeelde specificClass-basis, in vaste volgorde: configManager → configUtils → Logger → MeasurementContainer → ChildRegistrationUtils → ChildRouter. Default getOutput() = measurements.getFlattenedOutput(); notifyOutputChanged() emit 'output-changed' — het signaal waarop de adapter output stuurt.
src/domain/BaseDomain.js:24-137 (33-60,119-131)
MeasurementContainer
meetopslag · adressering
Chainable meetadministratie measurements[type][variant][position][childId] — de childId-laag is er altijd ('default' zonder .child()). Elke value() emit <type>.<variant>.<position> (positie lowercase); Measurement is een rolling window (10 samples). Zie demo I1.
src/measurements/MeasurementContainer.js:6-49,344-361,603-651
predict-engine
predictors · curves
Predict (tabulaire curve-predictor, y(x), getLocalPeak), Interpolation (1-D/3-D; monotone-cubic-spline — zie bug 5) en AffinityFlow (affiniteitswet ψ=ΔP/N², φ=Q/N; vereist gemeten astoerental).
src/predict/{predict_class.js:68, interpolation.js:53, affinityFlow.js:38-87}
configManager + configs
schema · runtime-config
13 schema-JSON's ({default, rules}). buildConfig voert een bewuste deep merge van de domainConfig uit (een eerdere shallow merge vaagde general.id weg). Validatie zit in ValidationUtils.validateSchema: onbekende keys eruit met warning, defaults erin. Versie-migratie stempelt 0.0.0→1.0.0.
src/configs/index.js:10-227 (97-172); src/helper/validationUtils.js:41-49
outputUtils + formatters
berichtopbouw · delta
formatMsg(output, config, format) met delta-compressie (alleen gewijzigde keys; niets gewijzigd → null) en tag-flattening. Pluggable formatters: influxdb, json, csv, process, frost. alwaysEmit omzeilt de compressie per veld (bv. pomp-ctrl).
src/helper/outputUtils.js:5-133; src/helper/formatters/index.js:20-52
commandRegistry + cmd-builder
input · verb-envelope
Declaratieve dispatch op de msg.command-verb-envelope — vervangt de per-node switch (msg.topic). cmd(verb)-builder waarvan de enumerable properties zélf de envelope zijn (geen .build(); methods niet-enumerable zodat de Node-RED-clone naar een plat object collapt).
src/nodered/commandRegistry.js:1-13; src/nodered/commandBuilder.js:1-30
state / movementManager
S88 · toestandsmachine
state is een EventEmitter-gebaseerde S88-achtige toestandsmachine; stateManager en movementManager sturen bewegingen: staticspeed (lineair) vs dynspeed (ease-in/out), met sequences (startup/shutdown/emergencystop) uit het config-schema.
src/state/{state.js:10, stateManager.js:39, movementManager.js:3}
registry / assetResolver
assets · curves · catalog
AssetResolver met namespaces (curves, assetSpec, menu, monsterSamples, monsterSpecs, units); sync resolve() (case-insensitive, warmt de hele namespace). Backends FileBackend/ HttpBackend; ensureModelCurve (edge refresh-on-select); loadCurve = deprecated shim.
src/registry/AssetResolver.js:20-60; src/registry/index.js:34-44
childRegistrationUtils
parent-child · handshake
Verwerkt child.register: valideert payload, normaliseert softwareType via SOFTWARE_TYPE_ALIASES (rotatingmachine→machine), ondersteunt meerdere parents, verrijkt de child-container (setChildId/Name/ParentRef) en slaat op onder mainClass.child[type][categorie]. Géén unregister-methode.
src/helper/childRegistrationUtils.js:15-123 (22-72)
ChildRouter + UnitPolicy
routing · canonieke units
ChildRouter: declaratieve parent-side routing (onRegister/onMeasurement/onPrediction) die per concreet event <type>.<variant>.<position> op de child-emitter abonneert; wildcards enumereren KNOWN_TYPES. UnitPolicy declareert de canonieke unit per domein en levert containerOptions().
src/domain/ChildRouter.js:64-169; src/domain/UnitPolicy.js:8-26
coolprop (WASM)
thermofysica
Gevendorde CoolProp Emscripten/WASM-build (~4,2 MB) via een vm-context met nagemaakte browseromgeving. API: PropsSI, verzadiging, dichtheid. EVOLV-uitbreiding: 'wastewater[:tss=g/L]'-correctie; dichtheid-fallback 1000. Exact 2 call-sites in heel EVOLV (pumpingStation, rotatingMachine).
src/coolprop-node/src/index.js:488; src/coolprop-node/src/cp.js:6-92
convert + Fysics
unit-conversie · hydraulica
Gevendorde fork van convert-units: fluent convert(v).from(u).to(u), 24 maatfamilies (o.a. pressure, volumeFlowRate, power, rotationalSpeed met anker rad/s). Fysics levert vloeistof/hydraulische helpers.
src/convert/index.js:4-140; src/convert/fysics.js:25
menu's
editor · client-JS
MenuManager genereert één client-side JS-string die window.EVOLV.nodes.<node> injecteert (menuData, HTML-injecties, initEditor). Geregistreerde factories: asset, logger, position, aquon. Legacy laag: menuUtils/endpointUtils.
src/menu/index.js:9-22,67-224
helpers
gravity · stats · pid · nrmse
gravity (WGS-84/Somigliana; standaard 9.80665), stats (mean/stdDev n−1/median; 1 sample→0 i.p.v. NaN), PIDController/CascadePIDController, ErrorMetrics (NRMSE), assertNoNaN. logger is een dunne console-wrapper (geen structured logging).
src/helper/{gravity.js:7-31, logger.js:1-54}; src/stats/index.js:15-28; src/pid/PIDController.js:15

I1 · Adresseren: type.variant.position.childId

Dit adresseringsschema deelt élke node. Kies de vier assen; de pagina bouwt live de event-naam die de container emit én de flattened output-key die op poort 0/1 verschijnt. Let op de twee valkuilen die de code expliciet benoemt: de positie wordt gelowercased (atEquipmentatequipment) en de .default-childId-laag hoort er altijd bij — hem weglaten is "the #1 footgun for new code". De keys hieronder zijn met node tegen de echte MeasurementContainer.js geverifieerd (zie testgevallen in de pagina-JS).

Adreschain
src/measurements/MeasurementContainer.js:258-290,344-361
Emit-eventop container.emitter
Waar ChildRouter._attachVariantListeners op abonneert — zo stromen child-metingen naar de parent zónder handmatige bedrading (src/domain/ChildRouter.js:129-145).
Flattened keypoort 0/1

I2 · Het poort 0/1/2-contract

Elke node erft dit poortcontract van BaseNodeAdapter: poort 0 procesdata (delta-gecomprimeerd), poort 1 dbase (Influx/FROST/…), poort 2 parent — eenmalig, 100 ms na constructie, het child.register-bericht. De outputloop wordt op twéé manieren bedraad. Kies hieronder de modus en zie welke draad actief is; de valkuil zit in de tick-modus.

parent MGC / pumpingStation / reactor BaseDomain notifyOutputChanged() emit 'output-changed' setInterval(tick) interval ms · source.tick() _emitOutputs() formatMsg × process/influx poort 0 process · delta poort 1 dbase · influx/frost poort 2 parent · register .on('output-changed') tick → _emitOutputs child.register · 100 ms · eenmalig
Volledige poort-referentie (keys per poort)
Poortcontract zoals BaseNodeAdapter + outputUtils het voor álle nodes afdwingen. measurement = config.general.name of fallback softwareType_id (src/helper/outputUtils.js:52-53); de formatter per kanaal is instelbaar via config.output.process/dbase.
PoortKanaalInhoud & belangrijkste keys
0process {topic: <naam>, payload} — delta-gecomprimeerd, alleen gewijzigde keys; keys in de flattened-vorm type.variant.position.childId (incl. .default). Object- velden ontlopen de delta-compressie (zie aandachtspunten). (src/helper/formatters/processFormatter.js:5-7; src/helper/outputUtils.js:17-41)
1dbase payload = {measurement, fields:<gewijzigd>, tags, timestamp} (default influxdb-formatter). Tags: id, softwareType, role, positionVsParent, uuid, tagcode, geoLocation, category, type, model, unit; geneste objecten geflattened tot key_childKey, lege waarden gedropt tegen tag-cardinaliteit. Omzetbaar naar frost (ISO-timestamp + source-blok voor coresync-SDT) / json / csv. (src/helper/formatters/influxdbFormatter.js:12-20; outputUtils.js:87-133; frostFormatter.js:8-28)
2parent Eenmalig, 100 ms na constructie: {topic:'child.register', payload:node.id, positionVsParent, distance} (positionVsParent default 'atEquipment'). De parent-handler roept childRegistrationUtils.registerChild(…) aan. (src/nodered/BaseNodeAdapter.js:117-131; src/helper/childRegistrationUtils.js:22-72)

De monotone-spline die niet monotoon wás — bug 5, nu gefixt

GEFIXT 2026-07-17 De predict-engine interpoleert curves met een monotone_cubic_spline. Tot 2026-07-17 was de Fritsch-Carlson- monotoniciteitslimiter gegate op method == "fritschcarlson", terwijl de type-string "monotone_cubic_spline" is: de limiter was dode code en er bleef een gewone cubic-Hermite over die op steile cliffs overshootte. De fix (c5f6224, PR #29, merge 6eaef76) laat monotone_cubic_spline nu wél door de Fritsch-Carlson-tangens-init én de limiter lopen (gate op twee plekken uitgebreid, src/predict/interpolation.js). Onderstaande figuur zet het oude gedrag (vóór de fix — overshoot naar 10,15 boven het plateau en −0,29 onder de vloer) naast de nu gedeployde monotone curve, die netjes binnen [0, 10] blijft — beide met node voorberekend (zie pagina-JS).

Aandachtspunten uit de code-analyse

Alle punten zijn code-observaties d.d. 2026-07-17, statisch tegen origin/main @ 70dd218. De testsuite draait groen (342 tests, 0 fail; zelf uitgevoerd 2026-07-17); CI draait npm test op node:20 maar heeft ondanks de jobnaam lint-and-test géén lint-stap.

Bug 5 · monotone-spline niet monotoon — GEFIXT
Gemerged in origin/main op 2026-07-17 (PR #29, commit c5f6224, merge 6eaef76). Was: de limiter gegate op method=="fritschcarlson" terwijl het type "monotone_cubic_spline" heet → dode code → cubic-Hermite met overshoot. De fix breidt de gate op twee plekken uit met || method=="monotone_cubic_spline", zodat de spline nu de Fritsch-Carlson-tangens-init én de limiter doorloopt; er is een interpolation-monotonic.test.js toegevoegd. Zie de vóór/ná-figuur hierboven.
src/predict/interpolation.js (gate ~303 + ~317)
Latente atEquipment-casusmismatch
Observatie (bevestigd aanwezig in origin/main): position() lowercased naar 'atequipment' (:287), maar _convertPositionStr2Num switcht op POSITIONS.AT_EQUIPMENT === 'atEquipment' (:839-853; src/constants/positions.js:8). Gevolg: .distance(null) na .position('atEquipment') valt in de default-branch (error-log, undefined) in plaats van 0. upstream/downstream matchen wél (al lowercase). aanname: mogelijk onbedoeld.
src/measurements/MeasurementContainer.js:287,839-853
Object-velden ontlopen delta-compressie
De opgeslagen waarde is ge-JSON.stringify'd, de verse waarde een object; !== is dus altijd waar en object-velden worden elke emissie opnieuw gestuurd (afgeleid randgeval; aanname dat dit onbedoeld is).
src/helper/outputUtils.js:26-40
Barrel-commentaar klopt niet
De header van index.js:6-7 belooft menuUtils als export, maar menuUtils, endpointUtils en nodeTemplates zitten níét in module.exports — alleen bereikbaar via een subpath/helper-barrel.
src/index.js:6-7,65-115
Dode package-export & CONTRACT-drift
package.json exporteert "./mathUtils" naar een niet-bestaand bestand. CONTRACT.md beschrijft formatMsg(payload, mode) (werkelijk: formatMsg(output, config, format)) en een niet-bestaand assetApiConfig. CONTRACT.md zegt zelf: de barrel wint (:116-117).
package.json; CONTRACT.md:38,54; src/helper/outputUtils.js:43
KNOWN_TYPES handmatig synchroon
ChildRouter moet meebewegen met MeasurementContainer.measureMap: "Keep in sync … if new types land there". Bij speed is een eerder incident gedocumenteerd — zonder de sync "silently drops every speed event" voor een wildcard-filter.
src/domain/ChildRouter.js:23-27,47-50
Geen unregister, geen dedupe
ChildRegistrationUtils heeft géén unregister-methode, en herregistratie pusht dezelfde child nogmaals in mainClass.child[type][category] (geen dedupe in _storeChild). De registeredChildren-Map dedupet wél op id. Hoe elke parent unregisterChild afhandelt is niet vastgesteld (per node; hypothese).
src/helper/childRegistrationUtils.js:58-63,74-86
Logger & licentie-mix
logger is een dunne console-wrapper (geen timestamps/JSON), ondanks het "structured logging"-label; MenuManager gebruikt rauwe console.warn/error. Repo-LICENSE is EUPL-1.2, maar validationUtils.js/configUtils.js dragen een eigen "no copying"-header, fysics.js een MIT-achtige, coolprop MIT; package.json zegt "SEE LICENSE".
src/helper/logger.js; src/menu/index.js:56,79; LICENSE

Volledige module-naslag (carry-over v1)

Uitklappen — modulepaden, units, integraties (uit v1)
ModulePadRol
MeasurementContainersrc/measurements/ Chainable meetopslag met events; Measurement is een rolling window (default 10); builder met verplichte type/variant/position. Twee unit-lagen: defaultUnits (ingress: pressure mbar, flow m3/h, power kW, temp C) en canonicalUnits (Pa, m3/s, W, K, speed rad/s).
outputUtilssrc/helper/outputUtils.js Berichtopbouw per kanaal met delta-compressie en pluggable formatters (influxdb/json/csv/process/frost).
childRegistrationUtilssrc/helper/childRegistrationUtils.js Parent-child-handshake: valideren, softwareType-aliasing, meerdere parents, opslag onder mainClass.child[type][categorie].
configManagersrc/configs/index.js Schema-gedreven configopbouw en versie-migratie (13 schema-JSON's); deep-merge domainConfig; validatie in ValidationUtils.
MenuManager + menuUtilssrc/menu/, src/helper/menuUtils.js Genereert client-side editor-JS met factories asset/logger/position/aquon.
convert + Fysicssrc/convert/ Gevendorde convert-units-fork (24 maatfamilies, rotationalSpeed anker rad/s) plus hydraulische helpers.
coolpropsrc/coolprop-node/ Gevendorde CoolProp Emscripten/WASM-build (~4,2 MB) met afvalwater-correctie en tabel-refrigerants.
predictsrc/predict/ Predict, Interpolation (1-D/3-D), AffinityFlow (affiniteitswet).
pid, nrmse, stats, state src/pid/, src/nrmse/, src/stats/, src/state/ Regelaars, foutmetrieken, statistiek, S88-toestandsmachine.
Platformklassensrc/domain/, src/nodered/ BaseDomain, BaseNodeAdapter, ChildRouter, UnitPolicy, LatestWinsGate, HealthStatus, statusBadge/StatusUpdater, CommandRegistry, cmd-builder.
Asset-registersrc/registry/ AssetResolver (namespaces assetSpec/curves/menu/monsterSamples/ monsterSpecs/units), FileBackend/HttpBackend, frostMeasuredCurve, loadCurve (deprecated shim).
Overigsrc/helper/, src/outliers/ configUtils, validation, assertNoNaN, gravity (WGS-84 Somigliana), logger; DynamicClusterDeviation alleen via subpath "./outliers".
Volledige module-naslag, overgenomen uit de v1-versie van dit artifact. De barrel (index.js:65-115) is het contractuele toegangspunt.

Parent-child (detail). De handshake loopt in drie stappen: het child stuurt (100 ms vertraagd, zodat siblings klaar zijn) child.register op poort 2; de flow-bedrading brengt dat bij de parent-input; de parent roept registerChild aan (bv. nodes/monster/src/commands/handlers.js:47-60). Daarbij normaliseert SOFTWARE_TYPE_ALIASES (rotatingmachine→machine, machinegroupcontrol→machinegroup). Parents in de praktijk: pumpingStation, machineGroupControl, valveGroupControl, monster, reactor, dashboardAPI.

CoolProp/WASM (detail). src/cp.js draait de Emscripten-glue in een vm-context met nagemaakte browseromgeving (mock XMLHttpRequest leest de .wasm van schijf) en pollt onRuntimeInitialized met 5 s timeout. Exact twee call-sites in heel EVOLV: pumpingStation/src/measurement/measurementRouter.js:5,71 (waterdichtheid) en rotatingMachine/src/prediction/efficiencyMath.js:21,98 (dichtheid voor rendement); de refrigerant-functies worden door geen enkele node aangeroepen.

Verder lezen

Bron statische code-analyse van de submodule generalFunctions. Het feitendossier is geschreven op werkkopie 14edf25941da (branch feat/child-router-speed-ctrl; super-repo RnD/EVOLV 74c4089, 2026-07-03). Analysedatum 2026-07-17.
Versheids-herverificatie op 2026-07-17 is git fetch gedraaid; de aandachtspunten zijn tegen origin/main gecontroleerd (baseline 70dd2180f9d0). Bug 5 is daarna gemerged: PR #29 (fix/monotone-cubic-overshoot, commit c5f6224) is via merge 6eaef76 in origin/main geland (CI lint-and-test + gitleaks groen), geverifieerd met git merge-base --is-ancestor c5f6224 origin/main → ancestor. De atEquipment-casusmismatch is nog bevestigd aanwezig in origin/main.
Methode statische code-analyse (geen runtime-verificatie, met uitzondering van het eenmalig draaien van npm test); elk feit draagt een pad:regel-verwijzing. De interactieve stukken zijn getrouwe herimplementaties: het adresserings-keyformaat (I1) is met node tegen de echte MeasurementContainer.js geverifieerd en de spline-figuur draait door de echte Interpolation-engine — testgevallen staan als commentaar in de pagina-JS.
Beperking beschrijft de geanalyseerde revisie; gedrag op andere branches of deployments kan afwijken.
Contact R&D-lab · lab.wbd-rd.nl