Waterschap Brabantse Delta · R&D-lab · uitleg

EVOLV · coresync — één meetwaarde naar FROST, zonder de server te overspoelen

eigenaar R&D-lab laatst getest 2026-07-17 versie 2.0 concept
coresync is de brug tussen de live EVOLV-plantflow en de FROST-server (OGC SensorThings API): het geeft elk asset één duurzame identiteit in het asset-register en streamt telemetrie ernaartoe zonder de server te overspoelen. De node wijkt bewust af van het standaardpatroon van de repo — geen command-envelope, geen tick-loop, geen 3-lagenpatroon: hij routeert op msg.topic en stuurt kant-en-klare HTTP-request-envelopes uit die een externe http request-node uitvoert. Deze pagina volgt letterlijk wat de code doet aan de hand van één meetwaarde en een interactieve figuur die de swinging-door-reducer getrouw naspeelt.

Doel & rol in de keten

coresync zit aan de rand van de flow van een site, stroomafwaarts van de measurement- en machine-nodes, en voedt FROST parallel aan de lokale InfluxDB-historian (docs/functional.html:146). Het is infrastructuur, geen procesequipment: er is geen S88-level-lane; palette-swatch #54647B "dark slate — infrastructuur" (coresync.html:4). Eén coresync per flow volstaat — de node multiplext alle streams op een eigen afgeleide sleutel.

coresync is anders dan de andere twaalf nodes. Houd dit vast bij het lezen:

geen command-envelope
Er is geen msg.command-verb en geen command registry — coresync routeert op msg.topic: telemetrie (al het overige), frost.response, model.set, coresync.flush (src/coreSyncDomain.js:57-68).
geen tick, geen 3 lagen
Nergens setInterval: volledig message-driven, output verschijnt alléén bij binnenkomende berichten. Geen nodeClass.js/specificClass.js; domein = coreSyncDomain.js (src/coreSyncDomain.js:28).
envelopes, geen eigen I/O
Het datapad doet zelf geen HTTP. Poort 1 = een array van FROST-request-envelopes voor een externe http request-node; poort 2 = altijd null (geen parent/child) (src/coreSyncDomain.js:85).

Rode draad door de pagina: één dbase-rij van een rotatingMachine — {measurement:'pump1', fields:{'flow.measured.downstream':42.0}, tags:{tagcode:'FT3'}}. De reis: normaliseren → stream-identiteit afleiden → swinging-door (ε-deur) → FROST-envelope → verplichte loopback frost.response → metadata-keten → Observation. De afgeleide sleutel wordt streamKey = FT3:flow:measured:downstream:<sensorTag>, met externalKeys thing:FT3, observedProperty:flow, datastream:<streamKey> (src/identity.js:38-79). De swinging-door-getallen in de figuur zijn met node nagerekend tégen src/reducer.js.

Praat met deze node

Het I/O-contract in één oogopslag. coresync kent geen verbs — kies een topic, stel de velden in, kopieer het exacte injecteerbare msg, en klik Injecteer ▸ om te zien wat er uit de drie poorten komt (met live afgeleide identiteit en getrouwe envelope-vormen). Elke bewering draagt een pad:regel-chip. Poortlabels ["process","dbase","parent"] (coresync.html:25-28).

Stuurinput · 1 poort · msg.topic
Bericht
Poortenoutput · 3 poorten

De drie envelope-vormen (echte shape)

Poort 1 is altijd een array van deze envelopes. Voor de rode-draadstream FT3 produceert de metadata-keten na elkaar een lookup, bij een lege lookup een create, en zodra alle ids bekend zijn de observation. {FROST_BASE} = de geresolvede FROST-basis-URL (config-node/env); de $filter rijdt op de wire URL-encoded; de Authorization-header (Basic) is hier gemaskeerd — hij komt uit de frost-server config-node en staat nooit in flow.json (src/frostRequests.js:19-112, coresync.js:117-121).

Volledige topic- & poort-referentie
Input-routing op msg.topic (src/coreSyncDomain.js:57-68); alleen velden met typeof value==='number' && Number.isFinite(value) tellen mee (src/normalizer.js:24).
msg.topicpayloadeffect
(al het overige) = telemetrieEVOLV dbase-rij {measurement, fields, tags, timestamp?/time?/ts?, source?} of array daarvanelk eindig-numeriek veld → stream → reducer → evt. Observation-requests (src/coreSyncDomain.js:70-86)
frost.responseantwoord van de http-request-node, met originele _coreSync-meta teruggeëchoodmetadata/observation-state vooruit; verplichte loopback — zonder blijft elke stream hangen op lookup (src/coreSyncDomain.js:88-136)
model.set{tag, catalogId:'<softwareType>:<modelId>', serialNumber?, reason?, by?, phenomenonTime?}PATCH Thing + append-only installedModel-Observation (src/modelSet.js:82-99)
coresync.flushelke stream stuurt zijn vastgehouden laatste punt als knot uit (src/coreSyncDomain.js:64-66)
Uitgaande poorten (return-shape altijd [port0, port1, port2]). dbaseFormat kent alleen de waarde 'frost' — er is geen Influx-formattering in deze node (coresync.html:66-69).
poortlabelinhoud
0processdiagnostiek; telemetriepad stuurt hier niets (src/coreSyncDomain.js:85). Wel: frost.error {streamKey, requestId, statusCode, kind, action} en model.set.done/.failed/.rejected; meerdere tegelijk → array (src/coreSyncDomain.js:281-296, src/modelSet.js:275-319).
1dbasealtijd een array van envelopes (of null): {topic, requestId, _coreSync, method, url, headers, payload}; topics frost.metadata.lookup/.create, frost.observation.create, frost.thing.patch (src/frostRequests.js:19-112, src/modelSet.js:222-242).
2parentaltijd null in deze build; geen parent/child-registratie, geen childRegistrationUtils (docs/contract.html:93).

Wie praat met wie — de verplichte lus

coresync doet zelf geen HTTP. Telemetrie komt van de measurement/machine-nodes binnen; poort 1 stuurt envelopes naar een externe http request-node, die FROST belt; het antwoord moet als frost.response terug de input in. Zonder die loopback hangt elke stream op zijn eerste lookup.

measurement / machine-nodes coresync deze node · infrastructuur in:1 · out:[p0,p1,p2] http request externe node (in de flow) FROST-server OGC SensorThings Things · Datastreams · … p0 diagnostiek debug / dashboard (meestal null) telemetrie p1: array envelopes HTTP frost.response — VERPLICHTE loopback → input p2 = altijd null
telemetrie in · via draad envelopes + verplichte frost.response-loopback p0 diagnostiek · meestal null

Naast het datapad kent coresync drie editor-time admin-endpoints (alle onder RED.auth.needsPermission('coresync.read')), die de tagpicker van een asset-node vóór Deploy aanroept: GET /coresync/allocate-tag (mint tag+uuid, idempotente rebind op bestaand uuid), GET /coresync/resolve-tag (bindt aan bestaand Thing, maakt nóóit aan) en GET /coresync/list-things (coresync.js:37-112). Foutcontract: 400 bij ontbrekende query-param, 502 bij FROST-fout. Deze endpoints bellen FROST wél direct via de eigen minimale HTTP-client (src/httpClient.js:3-8).

De reis van één meetwaarde naar FROST

Klik een stap in de lijst (of gebruik ◀ ▶). Boven zie je waar in de pijplijn je bent; rechts wat er gebeurt, de waarde van het rode-draadvoorbeeld op dat punt, en het codepad.

Swinging-door-compressie, interactief

Dit is de kern: hoe weinig punten FROST krijgt zonder de curve te verliezen. Kies een ruwe reeks, schuif de tolerantie ε (percentage van de meetwaarde) en de absolute vloer. De grafiek toont de ruwe samples (dicht), de overgebleven knots (weinig) en de lineaire heropbouw die Grafana/Influx zónder decoder tekent. De onderste grafiek bewijst dat de fout |heropbouw − origineel| onder de ε-band blijft voor élk sample. De reducer hieronder is een herimplementatie van src/reducer.js, vooraf met node geverifieerd (rechte ramp 200 samples → exact 2 knots; ε-garantie én knot-aantal-pariteit over ramp/driehoek/sinus/ruis — testcases als commentaar in de pagina-JS).

1,0 %
0,20
waarom ε/2-deuren
Elk punt spant een slope-band [(v−ε/2−v₀)/Δt, (v+ε/2−v₀)/Δt] op t.o.v. het anker; de banden worden geïntersecteerd. Loopt de intersectie leeg, dan wordt het vórige punt een knot en her-ankeren de deuren daar. Een lineaire redraw heeft max de deurafwijking als fout, dus ε/2-deuren houden de decoder-vrije redraw aantoonbaar binnen ε (src/reducer.js:155-190).
live resultaat

Waarom coresync afwijkt, en wat je moet weten

topic-routing i.p.v. verbs
Geen msg.command, geen command registry. De vier msg.topic-waarden zijn de enige "taal": telemetrie, frost.response, model.set, coresync.flush. De toestand zit per stream (id-cache, reducer, pending observations) (src/coreSyncDomain.js:57-68,148-168).
SDT ε/2-deur — bewijs
De deurhalfband is ε/2 zodat de plain-linear redraw in Grafana/Influx "provably within the user-facing tolerance ε of every original sample" blijft — geen decoder nodig. Getest: ramp 200→2 knots; ε-garantie over 5 signaalvormen (src/reducer.js:161-165, test/basic/coresync.basic.test.js:358-384).
identiteit & externalKeys
Eén duurzame identiteit per asset: streamKey + deterministische externalKeys (thing:<tag>, datastream:<streamKey>, …). De gemunte tag+uuid staat óók op het geprinte QR-label; het beoogde reconnect-pad is QR-scan (src/identity.js:63-79, coresync.js:94-96).
model.set — installedModel-historie
Legt vast welk catalogusmodel op een Thing zit: verse Thing-lookup (PATCH vervangt properties integraal), dan PATCH huidige stand + append-only Observation (OM_Observation, object-result). Warn-but-accept validatie; een onbekende tag is de enige harde fout — "model.set never creates Things" (src/modelSet.js:145-167,222-270).
dode / vestigiale velden
maxQueuedObservationsPerStream (config genegeerd, cap hardcoded op 2), diagnosticsEnabled (nergens gelezen), backoffFailures (geteld, nooit gebruikt), _coreSync.nextKind (gezet, nooit gevuld/gelezen). aanname deels onbedoeld (src/coreSyncDomain.js:39,132,252-262, src/frostRequests.js:39).
shared-entity registry & allocatierace
Eerste stream "bezit" de lookup/create; siblings wachten geparkeerd op het id (fix voor 6× thing:ft3 bij edge-startup, src/coreSyncDomain.js:43-51). De editor-time tag-allocatie is niet cross-instance veilig — prototype; de echte oplossing is een Postgres UNIQUE-index (src/allocator.js:14-18).

Configuratie

Editor-defaults in coresync.html:5-24, doorgegeven aan het domein in coresync.js:129-149. De belangrijkste velden:

Kernvelden; zie coresync.html:5-24 voor de volledige lijst (o.a. legacy angleToleranceDeg, timeScaleMs, dead-band guards minDeltaTimeMs/minDeltaValue). De config-node frost-server houdt username/password in Node-RED's encrypted store — nooit in flow.json (frostServer.js:9-10).
velddefaultwerking
frostServer / frostBaseUrl''FROST-verbinding; precedentie: config-node > veld > FROST_BASE_URL/FROST_VERSION env > localhost-fallback (src/frostConfig.js:24-48)
serviceVersion'v1.1'API-versie in het URL-pad (coresync.html:9)
comparisonMode'sdt'reducer-algoritme; angle/relative-slope zijn legacy, selecteerbaar voor al uitgerolde flows (src/reducer.js:4-9)
defaultTolerancePct1ε-percentage van de meetwaarde als de stream geen instrumentspec draagt (src/reducer.js:10-15)
toleranceFloor0absolute ondergrens ε (deadband nabij nul; coresync.html:92)
maxGapMs300000forceert een knot (reason:'max-gap') als het anker ouder is; 0 = uit (src/reducer.js:23,109-113)
burstWindowMs0sub-tick bursts binnen het venster samenvouwen tot één observatie; ~10 ms geadviseerd bij rotatingMachine-dubbelsamples (coresync.html:116, src/reducer.js:101-107)
assetTagOverride / sensorTagOverride''forceren thingTag/sensorTag boven tags/measurement (src/identity.js:51-61)

Aandachtspunten uit de code-analyse

Alle punten hieronder zijn code-observaties d.d. 2026-07-17 (statische analyse, niet in runtime geverifieerd); elk draagt een pad:regel.

Verder lezen

Bron statische code-analyse van de werkkopie RnD/EVOLV (super-repo 74c4089, 2026-07-03) — submodule coresync geanalyseerd @ 9d1d565da740 (de werkkopie wijkt af van de gitlink in het super-repo), plus het feitendossier van deze analyse. Analysedatum 2026-07-17.
Versheids-herverificatie op 2026-07-17 is git fetch gedraaid: de geanalyseerde tip 9d1d565da740 is een ancestor van origin/main (16384adc5209, 17 commits verder). De reducer-, identity-, frostRequests- en normalizer-modules zijn identiek aan origin/main; alleen coreSyncDomain.js is op main later vereenvoudigd (de shared-entity registry). De figuur en console zijn tegen de geanalyseerde revisie geverifieerd.
Methode statische code-analyse (geen runtime-verificatie); elk feit draagt een pad:regel-verwijzing. De swinging-door-figuur en de envelope-vormen zijn getrouwe herimplementaties, vooraf met node tegen src/reducer.js doorgerekend (ramp 200→2 knots; ε-garantie + knot-aantal-pariteit over 5 signaalvormen; testcases als commentaar in de pagina-JS).
Beperking beschrijft uitsluitend de geanalyseerde revisie; gedrag op andere branches of deployments kan afwijken. De tests van deze node draaien onder jest, niet node --test.
Contact R&D-lab · lab.wbd-rd.nl